Skip to content

fix(briefing,hooks): derive where the work stands; stop storing it - #211

Merged
kevintseng merged 3 commits into
mainfrom
fix/derive-status-not-store-it
Aug 24, 2026
Merged

fix(briefing,hooks): derive where the work stands; stop storing it#211
kevintseng merged 3 commits into
mainfrom
fix/derive-status-not-store-it

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

What prompted this

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 category error

memory status
what it is true once written true right now
goes stale never silently
who writes it someone said it the world changes
when wrong one fewer memory actively misleads

task_state is stored like the first and read like the second, so it can 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.

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. done becomes Had 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 runGit from why.ts" was wrong. It made repo-state un-mirrorable — generate-hook-core.mjs flattens 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 divergence briefing.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:

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 已發佈

It no longer claims to be the present.

Evidence

node scripts/run-tests-isolated.mjs   exit=0   190 files, 2607 tests
npm run verify:release                exit=0   all detectors new=0
npm run typecheck                     exit=0
npm run build                         exit=0

Break-tests, each restored byte-identical:

Mutation Result
derivation stubbed to a constant exit=1 — the case comparing against git's own count fails
best-effort catch removed exit=1 — 9 failed

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.

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.ts matches 0), and field values are whatever the user wrote; verified rendering with Japanese, German, Arabic (RTL) and emoji.

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]
@kevintseng
kevintseng merged commit a784e1d into main Aug 24, 2026
13 checks passed
@kevintseng
kevintseng deleted the fix/derive-status-not-store-it branch August 24, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant