fix(briefing,hooks): derive where the work stands; stop storing it - #211
Merged
Conversation
This session opened with memesh telling me:
Where "memesh" was left off (8 days ago):
- Just finished: v4.6.0 已發佈
- Next: UX-2:Search/Browse/Manage/Lessons 四面合一
38 PRs had merged since v4.6.0 and npm was serving 4.7.3. UX-2 was neither
done nor being worked on. Nothing was corrupted — every field was true when it
was written. The world moved and nothing told the store.
That is a category error, not a bug. A memory is true forever once written;
status changes without anyone saying so. `task_state` was stored like the
first and read like the second, so it could only ever report the moment
someone last spoke.
`src/core/task-state.ts` already carried a comment saying the age belongs in
the heading rather than being decoration — the staleness was anticipated, and
"(8 days ago)" was right there in the line that misled me. An annotation
cannot fix a sentence that asserts.
So status is derived now:
- `src/core/repo-state.ts` reads branch, uncommitted count, last tag, commits
since it, and whether the declared version has a tag — from local git, on
every call, never stored. Staleness becomes impossible by construction
rather than by discipline.
- The task-state heading attributes instead of asserting: `Stated about "X" 8
days ago, and not revisited since`. `done` is `Had just finished`, not
`Just finished`.
Local git only. Open-PR state would need auth, add latency to a hook budgeted
in seconds, and fail offline; local git answers "where am I" in milliseconds.
Best-effort throughout — no repository, no git, a timeout, all yield null and
the block simply does not appear.
Three things the tests caught that the plan had wrong:
1. The plan said reuse `runGit` from why.ts. Doing that made repo-state
un-mirrorable: `generate-hook-core.mjs` flattens paths as it copies, so a
mirrored module can only import mirrored ones, and the hook would have been
left deriving status differently from the CLI — the exact divergence
briefing.test.ts's parity case exists to forbid. repo-state owns its eight
lines of git wrapper and is a leaf.
2. Prepending the facts unconditionally broke "say nothing when there is
nothing to say": inside a git repository every call returned a fence whose
entire content was a branch name, which the agent can already see. The
facts prefix a briefing; they never constitute one.
3. `assembleBriefing(project)` takes a NAME, not a path, so `process.cwd()` is
the right repository for a CLI run and can be an unrelated one over MCP.
Reporting this repository's branch under another project's heading would
have been a new way of saying something false. Derivation is now skipped
when the process is not standing in the project being asked about.
Acceptance, on this repository:
Where the repository actually stands (read just now):
- branch main · 20 uncommitted
- at tag v4.7.3
Stated about "memesh" 8 days ago, and not revisited since:
- Had just finished: v4.6.0 已發佈
Same stored data. It no longer claims to be the present.
Deliberately not done: no coordination primitives (claims, leases, ownership).
git branches, PR state and CI already carry those, and they need the opposite
consistency guarantees from the best-effort, lossy, boundary-sampled design
that is correct for memory.
[Verified-By: node scripts/run-tests-isolated.mjs -> exit=0, "Test Files 190 passed (190)", "Tests 2607 passed (2607)"]
[Verified-By: npm run typecheck -> exit=0]
[Verified-By: npm run build -> exit=0, regenerating scripts/hooks/_generated/repo-state.js]
[Verified-By: break-test, status derivation stubbed to a constant -> exit=1, "Tests 1 failed | 11 passed (12)" on the case comparing against git's own count]
[Verified-By: break-test, best-effort catch removed -> exit=1, 9 failed; file restored byte-identical]
Herdr is a Rust runtime that owns coding agents' terminals so the work
survives a closed lid — 32k stars in five months, 807 plugins, YC F26. Its
marketplace indexes any public repository carrying the `herdr-plugin` topic
with a manifest, refreshed every 30 minutes, no review step. The memory niche
there is effectively empty: the largest memory-adjacent plugin has 9 stars and
is a Markdown notes tool.
Two entries, not the five that would fit.
panes briefing — the pane worth having in Herdr specifically. Running
several agents at once is the point of the runtime, and what they do
not share is what the project already learned. The topology reads
beside the panes doing the work.
actions doctor — answers the question a new install actually has, with
fixes rather than a status word.
`recall` was drafted and cut: `briefing` already carries recent activity, and
an argv-only action cannot take a search term, so it would have been a second
button producing a subset of the first. No [[build]] section either — a build
step would install a global npm package as a side effect of adding a plugin.
`min_herdr_version = "0.7.0"` is looked up, not copied from the example
manifest: 0.7.0 is the release whose CHANGELOG reads "Added local plugin v1
support ... managed plugin panes". `placement` is left at its default overlay,
so the 0.7.4 popup-sizing floor does not apply and 0.7.0 is the honest one.
The manifest declares its own `version` because the spec requires it, which
makes it a NINTH version anchor — and an unwatched version string is one that
drifts, as package-lock.json demonstrated by sitting four releases behind. So
check-version-coherence.mjs now covers it. The regex is anchored to the start
of a line because `min_herdr_version` also ends in `version`.
[Verified-By: python3 tomllib parse -> valid, required fields present, 1 pane + 1 action]
[Verified-By: memesh briefing --help / memesh doctor --help -> exit=0 each; both commands referenced by the manifest exist]
[Verified-By: node scripts/check-version-coherence.mjs -> exit=0, "herdr-plugin.toml version: 4.7.3", "All version sources agree." across 9 anchors]
[Verified-By: break-test, manifest version set to 4.7.2 -> exit=1, "herdr-plugin.toml (4.7.2) !== package.json (4.7.3)"; file restored byte-identical]
[Verified-By: npm run verify:release -> exit=0]
[Verified-By: node scripts/run-tests-isolated.mjs -> exit=0, "Test Files 190 passed (190)", "Tests 2607 passed (2607)"]
KT asked what happens without git. The module was written for it and the answer turned out to be right, but nothing was holding it there: the suite covered "not a repository" and "path does not exist" and called that the failure surface. Those are a DIFFERENT failure — git runs and answers "no" — from git not being resolvable at all, and only the first was pinned. Everyone developing this has git, so a regression here has no local symptom. It would surface on a slim container, a CI image without git, or a user who installed memesh to take notes and has never used version control. That is the worst place for it: readRepoState sits on the session-start path, so a throw takes the whole injection with it and the session loses its memories over a block that was optional to begin with. Verified by hand before writing the test, then pinned: PATH=<nonexistent> -> readRepoState() === null, repoStateLines() === [] memesh briefing with git off PATH -> exit 0, no repository block, memories still injected The test empties PATH rather than mocking, and asserts PATH was restored afterwards — without that, every later test in the file would silently be exercising a machine without git. [Verified-By: node scripts/run-tests-isolated.mjs -> exit=0, "Test Files 190 passed (190)", "Tests 2608 passed (2608)"] [Verified-By: npm run verify:release -> exit=0] [Verified-By: break-test, tryGit's catch replaced with a rethrow -> exit=1, "Tests 10 failed | 3 passed (13)" including the new no-git case; file restored byte-identical]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What prompted this
This session opened with memesh telling me:
38 PRs had merged since v4.6.0 and npm was serving 4.7.3. UX-2 was neither done nor being worked on.
Nothing was corrupted. Every field was true when it was written.
The category error
task_stateis stored like the first and read like the second, so it can only ever report the moment someone last spoke.src/core/task-state.tsalready carried a comment saying the age belongs in the heading rather than being decoration — the staleness was anticipated, and(8 days ago)was right there in the line that misled me. An annotation cannot fix a sentence that asserts.The change
src/core/repo-state.ts(new) derives, never stores: branch, uncommitted count, last tag, commits since it, and whether the declared version has a tag. Read from local git on every call, so staleness is impossible by construction rather than by discipline.The task-state heading attributes instead of asserting:
Stated about "X" 8 days ago, and not revisited since.donebecomesHad just finished.Local git only — open-PR state would need auth, add latency to a hook budgeted in seconds, and fail offline. Best-effort throughout: no repository, no git, a timeout → null, and the block simply does not appear.
Three things the tests caught that my plan had wrong
1. "Reuse
runGitfrom why.ts" was wrong. It made repo-state un-mirrorable —generate-hook-core.mjsflattens paths as it copies, so a mirrored module can only import mirrored ones. The hook would have derived status differently from the CLI: exactly the divergencebriefing.test.ts's parity case exists to forbid. repo-state owns its eight lines and is a leaf.2. Prepending the facts unconditionally broke "say nothing when there is nothing to say." Inside a git repository every call returned a fence whose entire content was a branch name — the one thing the agent can already see. The facts prefix a briefing; they never constitute one.
3.
assembleBriefing(project)takes a NAME, not a path.process.cwd()is the right repository for a CLI run and can be an unrelated one over MCP. Reporting this repo's branch under another project's heading would have been a new way of saying something false. Derivation is skipped when the process is not standing in the project being asked about.Acceptance
Same stored data, on this repository:
It no longer claims to be the present.
Evidence
Break-tests, each restored byte-identical:
exit=1— the case comparing against git's own count failsexit=1— 9 failedDeliberately not done
No coordination primitives (claims, leases, ownership). git branches, PR state and CI already carry those, and they need the opposite consistency guarantees from the best-effort, lossy, boundary-sampled design that is correct for memory.
Strings stay English, consistent with
config.ts's existing contract — the injected block is scaffolding for an agent, not UI. The dashboard's 11 locales don't translate it (dashboard/src/lib/i18n.tsmatches 0), and field values are whatever the user wrote; verified rendering with Japanese, German, Arabic (RTL) and emoji.