Skip to content

fix(add): make --update reachable for reference-kind configs (todos 757cefdb) - #57

Merged
andrei-hasna merged 1 commit into
mainfrom
757cefdb
Aug 4, 2026
Merged

fix(add): make --update reachable for reference-kind configs (todos 757cefdb)#57
andrei-hasna merged 1 commit into
mainfrom
757cefdb

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What

Fixes todos 757cefdb (the concretely-blocking half of it — scope note below).

instructions add <path> --kind reference --update could never find an existing row, at any setting. The collision check backing --update was findConfigsByTargetPath, which by design never matches a reference config (reference configs own no target_path — they are not mirrored 1:1 onto one file; they carry managed operating-rules content instead). add hardcoded existingOwners = [] whenever opts.kind === "reference", so every re-ingest of a reference config's content silently minted a fresh row instead of updating the one that already existed.

Evidence this is live damage, not hypothetical

Before this fix, the fleet store held 8 reference-kind rows named Global Agent Rules Standard (slugs -1 through -8) plus the canonical row — one identical SHA-256 content hash across all 8, ingested across 6 different days (2026-07-23 through 2026-08-02). That is this exact bug, already caught in the wild.

Independent confirmation: t42d493a5-driver hit and reported the same root cause on 2026-08-04 while landing an unrelated task (todos 757cefdb comment 47307cda), using an SDK-level workaround (resolveConfigStore().updateConfig()). This PR fixes the CLI path itself so that workaround is no longer necessary.

Fix

  • findReferenceConfigsByName (src/lib/config-target-identity.ts): a reference config's identity is its name (via slug), the same role target_path plays for file-kind configs. Matches on an exact name match (catches legacy duplicates like the 8 above, whose name is identical and whose slug only differs by the uniqueSlug -N suffix) or a slugified-name match (for a re-ingest whose --name differs only in case/punctuation).
  • add --update now calls store.createSnapshot before overwriting a row's content, whenever content actually changes — reusing the exact primitive apply.ts already uses before overwriting a rendered file on disk, so a content edit is recoverable via snapshot restore.
  • instructions doctor gains a findDuplicateReferenceNameGroups check, mirroring the existing duplicate-target-path check, so pre-existing collisions (like the 8 rows above) are visible instead of invisible.
  • The refusal message is now identity-aware (names the config by path for file-kind, by name for reference-kind; drops the sync suggestion for reference-kind, which sync doesn't cover).

Tests

TDD: src/cli/add-reference-update.test.ts reproduces the duplicate-row bug against the pre-fix code (3/5 cases failed — duplicate row created, no refusal, no snapshot); all 5 pass post-fix. Unit coverage for both new pure functions in src/lib/config-target-identity.test.ts. CLI-level coverage for the new doctor check in src/cli/doctor-reference-duplicates.test.ts.

bun test        -> 593 pass, 0 fail (was 583 pass before this branch; +10 new tests)
bun run typecheck -> clean

Scope — what this does NOT solve

The task's description also asks for "preserve provenance" and "require review before rollout." This PR does not add those:

  • No actor/author column exists on the configs table at all today; this PR does not add one.
  • No review gate exists between add --update succeeding and the change being live for the next apply/sync/session apply/push.

A full proposal → independent-review → rollout state machine was built for this exact row on 2026-07-17, across this repo and hasna/identities, with ~300 passing tests on both sides — and never merged. It is not recoverable: no trace in this repo's git log, branches, or worktrees, and the worktrees named in the original task comments no longer exist on this machine. Re-attempting that scope in this PR risked repeating why it stalled for 18 days. I'm filing the review/provenance gap as a separate, narrower follow-up task rather than reattempting it here.

Not merging this myself

Per fleet policy this needs an independent adversarial reviewer before merge. Leaving open for review.

Agent: t757cefdb-driver


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

… 757cefdb)

`instructions add <path> --kind reference --update` could never find an
existing row, at any setting. `add`'s collision check (`--update`'s only
identity signal) was `findConfigsByTargetPath`, which by design never
matches a reference config -- reference configs own no target_path, because
they are not mirrored 1:1 onto one file (they carry managed operating-rules
content instead). `add` hardcoded `existingOwners = []` whenever
`opts.kind === "reference"`, so every re-ingest of a reference config's
content silently minted a fresh row instead of updating the one that
already existed.

Measured live in the fleet store before this fix: 8 reference rows named
"Global Agent Rules Standard" (slugs -1 through -8), one identical SHA-256
content hash across all 8, ingested across 6 different days -- the exact
failure this fixes, already caught in the wild rather than hypothetical.

Fix: add `findReferenceConfigsByName`, using name (and its slug) as a
reference config's identity, the same role target_path plays for file-kind
configs. `uniqueSlug` only de-duplicates the SLUG column (which carries a DB
UNIQUE constraint); it never touches `name` (which carries none), so the
match is on EITHER an exact name match (to also catch legacy duplicates
already sitting in the store, whose name is identical and whose slug is a
`-1`/`-2`/... suffix) OR a slugified-name match (for a re-ingest whose
`--name` differs only in case or punctuation).

Also:
- `add --update` now snapshots a row's content before overwriting it
  (reusing the same `store.createSnapshot` primitive `apply.ts` already uses
  before overwriting a rendered file), whenever content actually changes --
  so a content edit is recoverable via `snapshot restore` even before
  anything is next applied or rendered.
- `instructions doctor` gains a `findDuplicateReferenceNameGroups` check,
  mirroring the existing duplicate-target-path check, so existing
  collisions like the one above are visible instead of invisible.
- The refusal message (no `--update` on a colliding identity) is now
  identity-aware: it names the config by path for file-kind and by name for
  reference-kind, and drops the `instructions sync` suggestion for
  reference-kind (sync only applies to disk-mirrored file-kind configs).

Root cause and regression coverage per Fix Once: TDD -- the new CLI-level
regression test (src/cli/add-reference-update.test.ts) reproduces the
duplicate-row bug against the pre-fix code (3 of 5 cases fail with a fresh
duplicate row and no snapshot); all 5 pass post-fix. Unit coverage for both
new pure functions in src/lib/config-target-identity.test.ts, plus a
CLI-level test for the new `doctor` check
(src/cli/doctor-reference-duplicates.test.ts). Full suite: 593/593 pass,
typecheck clean.

Scope, stated so it isn't oversold: this closes the concretely blocking half
of todos 757cefdb (a route that updates a managed instruction source without
minting a duplicate, in place, avoiding hand-edited generated files -- the
last of which was already true architecturally). It does NOT add a review
gate before rollout, and it does NOT add an actor/provenance column to the
configs table (none exists today) -- a full proposal -> review -> rollout
state machine was built once for this exact row in 2026-07-17 across this
repo and hasna/identities, fully tested, but never merged and is no longer
recoverable (verified: no trace in this repo's git history, branches, or
worktrees). That larger gap is filed separately rather than reattempted
here, to avoid repeating the scope that stalled it before.

Agent: t757cefdb-driver
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #57 @ 39b1cd7 — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1)

Candidate reviewed against freshly fetched origin/main at 03e2ee6.

What I ran:

  • git log --oneline origin/main..HEAD — exit 0; one commit, 39b1cd7.
  • git diff origin/main...HEAD --stat — exit 0; 5 changed files, 447 insertions / 15 deletions.
  • bun install — exit 0; setup only, 158 packages installed; not counted as a test gate.
  • bun run typecheck — exit 0; pass (tsc --noEmit; no test-case count applies).
  • bun run test — exit 0; 593 pass, 0 fail, 2471 expect() calls across 50 files.

What I read:

  • The full diff of all five changed files: src/cli/add-reference-update.test.ts, src/cli/doctor-reference-duplicates.test.ts, src/cli/index.tsx, src/lib/config-target-identity.test.ts, and src/lib/config-target-identity.ts.
  • Surrounding CLI add/doctor/snapshot flows; ConfigStore local and HTTP implementations; server snapshot routes; SQLite/PostgreSQL slug, config, and snapshot code; reference-config session-render selection; the managed global-rules source; and the MCP create-config ownership guard.

Blocking P0/P1 findings: none. The reference-name lookup reaches the existing row, refuses accidental duplicate adds by default, updates in place only with --update, and snapshots changed content before mutation. The snapshot method exists on both store transports, and the declared typecheck/test gates pass.

Non-blocking follow-up:

  • The separate MCP create_config path still exempts all reference-kind rows from identity collision checks, so it can continue creating same-name reference duplicates. That behavior predates this add-command-specific change and is outside this PR's stated acceptance scope; it should be aligned separately if the product-wide invariant becomes “one reference name, one row.”

@andrei-hasna
andrei-hasna merged commit e85f23d into main Aug 4, 2026
3 checks passed
@andrei-hasna
andrei-hasna deleted the 757cefdb branch August 4, 2026 10:36
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #57 @ 39b1cd7 — lens: reference-config identity and store safety, reviewer instructions-pr57-reviewer (1 of 1)

What I actually ran, not just read

Worktree: $HOME/.hasna/repos/worktrees/open-instructions/pr57-review-verify, created via repos worktree add against repo catalog id 1542 (canonical checkout /home/hasna/workspace/hasna/opensource/open-instructions, remote github.com/hasna/instructions), base 757cefdb resolved from origin, base_sha 39b1cd7d808e003796185cb7def310b1a05f0f37 — matches the PR head exactly. I never touched the driver's pr57-review worktree/lock.

Merge-base check per the base-move rule: git rev-parse origin/main = 03e2ee6119440cbb33a44a35055fb02a95633c7e, identical to the PR's actual merge-base (git log on the fetched head shows 39b1cd7 on top of 03e2ee6 directly). Main has not advanced since; no merge-tree staleness check was needed.

git diff --stat 03e2ee61..39b1cd7d (my own re-derivation, not copied from the PR object): 5 files changed, 447 insertions(+), 15 deletions(-) — matches the stated +447/-15 across 5 files exactly.

1. Root cause, read at the line

src/cli/index.tsx pre-fix (git show 03e2ee61:src/cli/index.tsx) had:

const existingOwners = opts.kind === "reference" ? [] : findConfigsByTargetPath(...)

Confirmed this is the actual pre-fix code, not a restatement, by checking it out into my worktree and running the new regression file against it.

2. TDD claim, verified by actually reverting the fix

git checkout 03e2ee61 -- src/lib/config-target-identity.ts src/cli/index.tsx (new test files left at HEAD), then:

bun test src/cli/add-reference-update.test.ts

Output: 2 pass / 3 fail, the three failures being exactly the ones the commit message names — expect(second.status).not.toBe(0) (got 0: no refusal happened), and two rows.length assertions expecting 1 and getting 2 (a fresh duplicate row was minted both on a bare --kind reference add and on --update). doctor-reference-duplicates.test.ts also failed 0/2 pre-fix (the doctor check literally doesn't exist without the fix).

Restored git checkout HEAD -- <same two files> (verified git status --short empty after), reran the same two files: 7 pass, 0 fail (5 + 2). This is 3 of 5 failing pre-fix, 5/5 passing post-fix — independently reproduced, not relayed.

Full suite: bun test593 pass / 0 fail / 2471 expect() calls, run twice for a stable re-read. bun run typecheck → rc=0, no output. Matches the commit message.

3. Does the snapshot actually restore? Yes — exercised end to end, not just asserted

In an isolated temp store (HASNA_INSTRUCTIONS_DB_PATH + CONFIGS_HOME pointed at a scratch dir, HASNA_INSTRUCTIONS_API_URL/_API_KEY explicitly unset for the child process — never touched the live store):

add rule.md --name snap-test-rule --kind reference          -> content "v1 content"
add rule.md --name snap-test-rule --kind reference --update -> content "v2 content DIFFERENT"
snapshot list <id>   -> "v1  2026-08-04T10:29:03.483Z  be75c7ab-..."   (1 snapshot)
snapshot show <snap> -> "v1 content"                                   (pre-overwrite content, intact)
snapshot restore <id> <snap>
list --json           -> {"content":"v1 content\n","version":3}        (content genuinely reverted)

The recovery primitive works, not just exists. Also checked the "no spurious snapshot" half: --update with byte-identical content creates zero snapshots (snapshot list -> "No snapshots.") — the content !== target.content guard at the call site holds.

4. Does doctor detect the real, live damage shape? Yes, reproduced at 1:1 scale with the incident

Reverted the two source files again, ran add ... --name "Global Agent Rules Standard" --kind reference three times against a fresh store with pre-fix code (no --update needed — pre-fix add never refuses reference-kind at all), producing three rows: global-agent-rules-standard, -1, -2 — the identical shape to the measured 8-row live incident. Restored the fix, ran doctor:

✗ 1 reference name(s) claimed by more than one row (3 rows) — only one is live in the next render
      Global Agent Rules Standard
        global-agent-rules-standard    (3921b8a6-...)  updated ...
        global-agent-rules-standard-1  (023408ac-...)  updated ...
        global-agent-rules-standard-2  (bfc61a1d-...)  updated ...
      Keep one row per name: `instructions delete <id>` for the extras.

Then confirmed the actual remediation path works: add with corrected content + --update on that same name updates the first row and prints ⚠ 2 other row(s) still share this name: ..., ... — exactly the workflow an operator needs to reconcile the live 9-row incident (update with correct content, then delete the two named extras).

5. Is name/slug a safe identity? Yes, by the same argument that already justifies target_path

Two different reference documents that happen to be given the same --name collide and become "one identity," exactly as two different files given the same target_path already collide today. That is existing, accepted precedent (the refusal-without---update behavior is unchanged in spirit, just re-scoped to a name axis) — not a new risk this PR introduces. I don't consider this unsafe.

Two real findings, neither blocking this merge

P1 (real, reproducible, in-scope, non-blocking) — findDuplicateReferenceNameGroups (config-target-identity.ts:79) groups on exact config.name, while the PR's own identity matcher findReferenceConfigsByName (config-target-identity.ts:65) matches on name OR slugified name. The doctor check and the --update identity check use two different axes of "the same identity," and the narrower one is the one that reports "clean."

Reproduced: seeded a store with two reference rows that are the same identity by the PR's own slug-based definition but differ in exact name string ("My Rule" / "my rule", slugs my-rule / my-rule-1 — the exact shape uniqueSlug produces for a case-variant re-ingest). doctor output, verbatim:

  ✓ No reference config name is claimed by more than one row

— a false "clean," while the two rows sit in the same store. Then ran add "MY RULE" --kind reference --update against that same store: it updated only the my-rule row (✓ Updated: My Rule (my-rule)) and printed no warning at all — the rest.length > 0 branch never fires because findReferenceConfigsByName never put the my-rule-1 sibling into existingOwners in the first place (it matches config.slug === wantedSlug — the row's own literal slug — not "does this row's name slugify to the same value," so a previously-suffixed sibling is invisible to a query keyed on yet another spelling). After the update, my-rule-1 sits there unreported, unrouted, holding stale content, and list --json shows it right alongside the "reconciled" row.

Why this doesn't block: the actually-measured live incident (8 rows, byte-identical name string) is the exact-match shape, which this PR catches perfectly — I reproduced that at 1:1 scale in §4 above and it works. This finding is about an adjacent identity shape (case/punctuation-variant names sharing a slug root) that the PR's own doc comment explicitly says it handles ("Slugified name match, for a re-ingest whose --name differs only in case or punctuation") but the duplicate-detector and the update's own rest-warning don't actually cover. I found no evidence this shape has occurred live yet (the measured 8+1 are all byte-identical names). Recommend: change findDuplicateReferenceNameGroups's grouping key from config.name to slugify(config.name), and reconsider whether findReferenceConfigsByName should also match on slugify(config.name) === wantedSlug (not just the stored config.slug) so a family of suffix-variant rows is found as a whole. Searched todos --status pending --limit 500 and gh pr list --repo hasna/instructions --search "reference duplicate OR slug OR doctor" first — nothing tracks this; it isn't a duplicate.

P2 (real, pre-existing, extended by this PR's new tests, non-blocking, does not affect CI) — the new unit tests in config-target-identity.test.ts call bare getDatabase() in-process without clearing ambient HASNA_INSTRUCTIONS_API_URL/_API_KEY, and the guard in src/db/database.ts:109 keys on whether an explicit path argument was passed, not on whether HASNA_INSTRUCTIONS_DB_PATH is set — so the test's own beforeEach setting HASNA_INSTRUCTIONS_DB_PATH=":memory:" does not satisfy the guard.

On station01, where HASNA_INSTRUCTIONS_API_URL/_API_KEY are ambient (this fleet's normal working state — confirmed via [ -n "$VAR" ] checks, values never printed):

bun test src/lib/config-target-identity.test.ts   ->  0 pass, 13 fail, every failure the same
  "error: instructions is in self_hosted (cloud) mode: ... Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY ..."

Yet the bare full-suite run (bun test, no args, run twice) is genuinely 593 pass / 0 fail with zero occurrences of that string. Forcing bun test --isolate (bun's own flag for "run each file in a fresh global object; state from one file cannot affect another") on the full suite: 371 pass / 222 fail, 338 occurrences of the same guard error — reproduced twice, both directions. So the full-suite green on a station is real but is order-dependent: some earlier file's env handling happens to leave HASNA_INSTRUCTIONS_API_URL/_API_KEY cleared for the rest of the process (bun does not isolate test files from each other by default), and this PR's new unit tests ride on that same accident rather than clearing the vars themselves the way src/mcp/mcp.test.ts already does (save/delete/restore in beforeEach/afterEach).

This does not touch CI: .github/workflows/ci.yml runs bun test bare with no env: block naming these variables, and GitHub Actions runners never have them ambient, so the guard's condition is structurally false there regardless of file order. It also doesn't affect the two CLI-level regression test files this PR added (add-reference-update.test.ts, doctor-reference-duplicates.test.ts) — they spawn bun src/cli/index.tsx as a subprocess with HASNA_INSTRUCTIONS_API_URL: undefined, HASNA_INSTRUCTIONS_API_KEY: undefined explicitly set in the child env, so they're immune regardless of parent-process state; I ran both in isolation with my ambient vars still set and they passed cleanly (7/7 combined). This is a pre-existing pattern in ~13 other files in this repo (any bare getDatabase()-calling test file), not something PR #57 introduced — it's just that this PR adds 8 more tests following the same fragile pattern. Non-blocking; worth a fast-follow (either fix the 13+ files' isolation, or fix the guard to also honor HASNA_INSTRUCTIONS_DB_PATH when it names an explicit override) so that a station agent debugging an unrelated failure in this repo doesn't get misdirected by 222 unrelated failures if they ever add --isolate or run a subset of files, which is exactly the working pattern I used to verify §2 above.

What I did NOT check

  • Did not verify behavior against a real PostgreSQL-backed store (only SQLite, via HASNA_INSTRUCTIONS_DB_PATH) — the diff doesn't touch anything backend-specific, so I don't think this matters, but I didn't exercise it.
  • Did not check the MCP surface (src/mcp/) for an equivalent reference-kind update path that might have the same pre-fix bug outside the CLI — the diff only touches src/cli/index.tsx, so if instructions-mcp has its own add/update_config tool with independent collision logic, it may still carry the original bug. I did not go looking for one.
  • Did not attempt to enumerate or touch the live fleet store's actual 9 duplicate rows — my dispatcher's brief already established that count independently; I verified the mechanism and the remediation workflow against reconstructed, disposable, local-only data instead, per the standing freeze on live-store mutation.
  • Did not check --isolate's interaction with --parallel, sharding, or CI-adjacent invocations beyond confirming the current ci.yml doesn't set the vulnerable env vars at all.

Verdict

GO. The concretely-measured, live incident this task exists for (exact-name reference-config duplication, unreachable --update) is fixed, and I reproduced the fix working end to end — refusal, update-in-place, snapshot-and-restore, and doctor detection — against reconstructed data matching the real incident's shape, not just by reading the driver's assertions. The two findings above are real but neither is material to that acceptance: one is an adjacent identity shape with no observed live instance, the other is a pre-existing test-harness fragility that doesn't reach CI. Recommend filing both as separate, named follow-up tasks rather than deferring silently.

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