@event4u/agent-config 9.34.0 → 9.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +91 -0
  3. package/CONTRIBUTING.md +23 -14
  4. package/dist/agent-src/commands/roadmap/next.md +37 -5
  5. package/dist/agent-src/scripts/roadmap_gates.ts +119 -4
  6. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +25 -0
  7. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  8. package/dist/agent-src/templates/scripts/memory_lookup.ts +1 -2
  9. package/dist/agent-src/templates/scripts/memory_signal.ts +2 -4
  10. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  11. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  12. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  13. package/dist/cli/registry.js +2 -2
  14. package/dist/cli/registry.js.map +1 -1
  15. package/dist/cli-delegate/{chunk-REMEZSVP.js → chunk-2TGMGLBL.js} +2 -2
  16. package/dist/cli-delegate/{chunk-YPAWXXXR.js → chunk-55RI5UXK.js} +17 -0
  17. package/dist/cli-delegate/{chunk-MA4GFWDY.js → chunk-H6F2CQFI.js} +1 -1
  18. package/dist/cli-delegate/{chunk-UVKNIJH2.js → chunk-ZAXH3ZWU.js} +3 -0
  19. package/dist/cli-delegate/cmd_conformance.js +2 -2
  20. package/dist/cli-delegate/cmd_doctor.js +2 -2
  21. package/dist/cli-delegate/cmd_export.js +2 -2
  22. package/dist/cli-delegate/cmd_handoff.js +1 -1
  23. package/dist/cli-delegate/cmd_prune.js +2 -2
  24. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  25. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  26. package/dist/cli-delegate/cmd_sync.js +2 -2
  27. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  28. package/dist/cli-delegate/cmd_validate.js +2 -2
  29. package/dist/discovery/deprecation-report.md +1 -1
  30. package/dist/discovery/discovery-manifest.json +4 -4
  31. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  32. package/dist/discovery/discovery-manifest.summary.md +1 -1
  33. package/dist/discovery/dormancy-report.md +1 -1
  34. package/dist/discovery/orphan-report.md +1 -1
  35. package/dist/discovery/packs.json +2 -2
  36. package/dist/discovery/trust-report.md +1 -1
  37. package/dist/discovery/workspaces.json +2 -2
  38. package/dist/hooks/dispatch.js +259 -78
  39. package/dist/install/install.mjs +3 -0
  40. package/dist/mcp/registry-manifest.json +2 -2
  41. package/dist/mcp/server.json +2 -2
  42. package/dist/mcp/server.mjs +74 -2
  43. package/dist/server/schemas/settings.js +1 -0
  44. package/dist/server/schemas/settings.js.map +1 -1
  45. package/docs/contracts/settings-classes.md +3 -2
  46. package/package.json +1 -1
  47. package/src/agent-src/scripts/roadmap_gates.ts +119 -4
  48. package/src/agent-src/templates/scripts/memory_lookup.ts +1 -2
  49. package/src/agent-src/templates/scripts/memory_signal.ts +2 -4
  50. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  51. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  52. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  53. package/src/config/agent-settings.template.yml +11 -2
  54. package/src/config/quorum-attendance-budget.json +10 -2
  55. package/src/scripts/_dispatch.bash +6 -2
  56. package/src/scripts/_lib/session_register.ts +67 -1
  57. package/src/scripts/ai_council/config.ts +60 -0
  58. package/src/scripts/ai_council/events_log.ts +103 -4
  59. package/src/scripts/ai_council/quorum.ts +83 -6
  60. package/src/scripts/council_cli.ts +36 -1
  61. package/src/scripts/hooks/turn_end_gate_hook.ts +172 -11
  62. package/src/scripts/lint_framework_leakage.ts +195 -28
  63. package/src/scripts/lint_framework_leakage_allowlist.json +94 -86
  64. package/src/scripts/memory_lookup.ts +1 -2
  65. package/src/scripts/schemas/command.schema.json +4 -0
  66. package/src/scripts/schemas/skill.schema.json +4 -0
  67. package/src/scripts/session_register_hook.ts +238 -18
  68. package/src/scripts/sessions_cli.ts +377 -12
  69. package/src/server/schemas/settings.ts +3 -0
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Bootstrap shim for the event4u agent-config suite. This plugin ships ONLY the dispatcher hooks plus one install-pointer skill — ALL content (skills, rules, commands) is installed via `npx -y @event4u/agent-config init`, which writes the file projection AND registers the same hooks in ~/.claude/settings.json. DEPRECATED as a content channel; kept so the marketplace listing stays discoverable and existing installs keep working hooks.",
9
- "version": "9.34.0",
9
+ "version": "9.35.0",
10
10
  "keywords": [
11
11
  "agent-config",
12
12
  "skills",
package/CHANGELOG.md CHANGED
@@ -330,6 +330,97 @@ Entry-shape contract: [`docs/contracts/CHANGELOG-conventions.md`](docs/contracts
330
330
  > that forces a new era split (`# Era: 9.35.x`, etc.) — see
331
331
  > [`docs/contracts/CHANGELOG-conventions.md § Era splits`](docs/contracts/CHANGELOG-conventions.md).
332
332
 
333
+ ## [9.35.0](https://github.com/event4u-app/agent-config/compare/9.34.0...9.35.0) (2026-08-12)
334
+
335
+ ### Release highlights
336
+
337
+ <!-- Curated head: fill before merge, keep it under 10 lines, and leave `_none_` where it is genuinely the answer. The generated log below is unchanged. -->
338
+ - **Behaviour changes:** _auto-derived, rewrite before merge:_ rule/schema diffs, breaking commits or removed public surface in af968b5, 6d3c9f7.
339
+ - **Default changes + migration:** _none_
340
+ - **Security and correctness:** _none_
341
+ - **Honest nulls:** _none_
342
+ - **Known limitations:** _none_
343
+
344
+ ### Features
345
+
346
+ * **leakage-allowlist:** validate the exemptions before scanning, retire position keys ([97d2c29](https://github.com/event4u-app/agent-config/commit/97d2c29ba763af06c45c5e4a4aebbed7fb531561))
347
+ * **sessions:** refuse a claim a peer holds, and report the branch axis ([39cb3b4](https://github.com/event4u-app/agent-config/commit/39cb3b4f2e6d3c0602e5544b7079f242f5a7cd9f))
348
+ * **council:** record the ADR-224 solo floor without enforcing it ([be09d0f](https://github.com/event4u-app/agent-config/commit/be09d0fa8f75e03b3d767f225d67cda53dd36ee8))
349
+ * **turn-end-gate:** refuse a turn that edited a file and verified nothing ([647eca9](https://github.com/event4u-app/agent-config/commit/647eca9830a6ea9b6597a3404f37aac2fe41360d))
350
+ * **gates:** enumerate staged confirmations behind gates --pending ([9cccf4e](https://github.com/event4u-app/agent-config/commit/9cccf4eb789f68dcc67bbf8534824212ea507845))
351
+ * **work-engine:** exactly-once confirmed execution for a staged action ([8588e54](https://github.com/event4u-app/agent-config/commit/8588e54377062126df888b42f02155ad9e28c3b5))
352
+ * **schemas:** add requires_confirmation to the skill and command contracts ([af968b5](https://github.com/event4u-app/agent-config/commit/af968b533a72c2358897fabcfe61bfb1baa7dc87))
353
+ * **cli:** hooks:status --pending enumerates staged confirmations ([442bde3](https://github.com/event4u-app/agent-config/commit/442bde3bb0b047ca85d8b0e5dcb973af4343209c))
354
+ * **work-engine:** exactly-once confirmation store plus a decision-gate seam ([9fe068c](https://github.com/event4u-app/agent-config/commit/9fe068c96a698c1d30ae74d65545b9fa301e72a3))
355
+ * **schemas:** declare requires_confirmation on skills and commands ([6d3c9f7](https://github.com/event4u-app/agent-config/commit/6d3c9f73ccc224517a81effb5f9874f388e140ee))
356
+
357
+ ### Bug Fixes
358
+
359
+ * **tests:** a matched grep must not fail the assertion — echo | grep -q under pipefail ([e7352cc](https://github.com/event4u-app/agent-config/commit/e7352ccedbd9cd996220db0293faf6d42a67529e))
360
+ * **leakage-allowlist:** close the seven R2 findings, including two on this PR ([c859249](https://github.com/event4u-app/agent-config/commit/c8592492f32d6c5072eb361fde9081457855d5a7))
361
+ * **sessions:** drop the ROADMAP_CLAIM_REL import that lost its last reference ([0d6a1c0](https://github.com/event4u-app/agent-config/commit/0d6a1c0e4670a30f7f00475d7463b986f8fa697b))
362
+ * **session-register:** work in the PR review — path containment, token-boundary match, claim lifetime ([8e108ca](https://github.com/event4u-app/agent-config/commit/8e108ca07bae79a6058608a4bfd4b316e8d12e31))
363
+ * **testing-anti-patterns:** close the R2 findings on the new anti-pattern ([67b747e](https://github.com/event4u-app/agent-config/commit/67b747e95bfdbf2026f5e8286a960045eb864568))
364
+ * **session-register:** all 15 R2 findings, three of them the same shape as the bug ([871c00a](https://github.com/event4u-app/agent-config/commit/871c00a6275ed0ecf6bc3a9ea2783aa134388ec5))
365
+ * **session-register:** key the roadmap claim on the session, and reject a stale slug ([64a9ce7](https://github.com/event4u-app/agent-config/commit/64a9ce71ad4a410fdcbed361fb8a2829c4a306ac))
366
+ * **session-register:** compare the roadmap, not the branch name ([e338113](https://github.com/event4u-app/agent-config/commit/e338113b0adcb8d5ccb4239478bb9f2033cbc2e4))
367
+ * **staged-confirmation:** close the round-2 findings on the hardening itself ([fa47ef6](https://github.com/event4u-app/agent-config/commit/fa47ef6253b4ae1d1612db972ed39c856ee29314))
368
+ * **staged-confirmation:** sound the type guard and refuse a traversing token ([75dbdb3](https://github.com/event4u-app/agent-config/commit/75dbdb35fce020568fbeba978f14b521599aee67))
369
+ * **test:** stop pipefail reporting a matched grep as a failure ([29916d1](https://github.com/event4u-app/agent-config/commit/29916d1e96e6118d9a15e84cc6eeac6de3516705))
370
+ * **council:** record min_present on the quorum line, schema v3 to v4 ([53fe036](https://github.com/event4u-app/agent-config/commit/53fe0362b2d403455b6f7eb4e66952b1dd42e161))
371
+ * **cli:** refuse hooks:status --pending --strict instead of exiting 0 ([7e752a9](https://github.com/event4u-app/agent-config/commit/7e752a9d6da83b7c73d97cb2996fbc7d465caf4c))
372
+ * **confirmation:** close R2 findings 1-5 and 7 in the store and the seam ([42c6f5b](https://github.com/event4u-app/agent-config/commit/42c6f5b5b4e9f2c5835dbbf14567a6944bab13dc))
373
+ * **council:** judge the shadow floor against the configured roster ([9cde092](https://github.com/event4u-app/agent-config/commit/9cde092cf6c745c55d9468ebb4480ffb2a11259a))
374
+ * three CI reds — a raw NUL in a source, and a settings key declared once ([b128645](https://github.com/event4u-app/agent-config/commit/b128645e8aa29e1b8ccbab31fd3b5509e73da0da))
375
+
376
+ ### Documentation
377
+
378
+ * **review:** re-bind the review artefact after the pipefail fix ([d4b23e1](https://github.com/event4u-app/agent-config/commit/d4b23e1c9f2620778564d81c7e0c3180e3b60eb8))
379
+ * **review:** re-bind the review artefact after merging main ([3fc625f](https://github.com/event4u-app/agent-config/commit/3fc625f8b5d02320f62af217f7805cf85c95e91d))
380
+ * **review:** re-bind the completion-review artefact to the fixed scope ([0fc9e82](https://github.com/event4u-app/agent-config/commit/0fc9e822c2f863216f7e721f471b070d50718157))
381
+ * **review:** record the R2 findings before any fix ([9c66161](https://github.com/event4u-app/agent-config/commit/9c66161c76484a856b1293918074bcf387683d9b))
382
+ * **testing-anti-patterns:** the negative test that passes for the wrong reason ([8fb3f84](https://github.com/event4u-app/agent-config/commit/8fb3f84a7a9c009a53dd383ac8a86bfe00de4fe6))
383
+ * state the collision that cost a PR, and the two axes that catch it ([60c0df8](https://github.com/event4u-app/agent-config/commit/60c0df808af87a748643d1fbfe2310a9a08f2c25))
384
+ * **review:** record the R2 completion-review findings before fixing them ([35570a5](https://github.com/event4u-app/agent-config/commit/35570a5df0be0cd402f8cc9ed3e5f5cf817a02d1))
385
+ * **roadmap:** close and archive the solo-floor roadmap ([128b2cd](https://github.com/event4u-app/agent-config/commit/128b2cddbadf12825bb220cc2aef261976ccb023))
386
+ * **adr:** answer ADR-224 three open questions in an amendment ([ff2362e](https://github.com/event4u-app/agent-config/commit/ff2362e1971dadd3b547fbfeadf2650dbb3a4c3f))
387
+
388
+ ### Refactoring
389
+
390
+ * **leakage-allowlist:** key 35 exemptions on content, drop 3 that were dead ([de96358](https://github.com/event4u-app/agent-config/commit/de96358ddb309be1d4ffa4035d3b8e55e8e08458))
391
+
392
+ ### Tests
393
+
394
+ * **session-register:** pin all five failure modes, each with the case that was silent ([7f35c3b](https://github.com/event4u-app/agent-config/commit/7f35c3bce8118d3112217af6d20dabdae487b301))
395
+ * **schemas:** validate the command surface instead of reading its shape ([5b75e64](https://github.com/event4u-app/agent-config/commit/5b75e64cc6f988212aabaaa84e82ebb4940681e0))
396
+
397
+ ### Build
398
+
399
+ * refresh dist/install/install.mjs for the new settings key ([a4a99ed](https://github.com/event4u-app/agent-config/commit/a4a99edd6132f92d6e975ccba3261939964c015b))
400
+
401
+ ### Chores
402
+
403
+ * **review:** re-bind the artefact and mark all seven findings fixed ([fb6283f](https://github.com/event4u-app/agent-config/commit/fb6283fa2011650986c89eb514e0be80d2ffd9c0))
404
+ * **memory:** drop three docblock claims about the removed agent-memory path ([307a355](https://github.com/event4u-app/agent-config/commit/307a3550fce6e81dfeb42b0124e276b762da9d66))
405
+ * **review:** record seven R2 findings before fixing them ([c86bf3d](https://github.com/event4u-app/agent-config/commit/c86bf3daa3e3281416c9ce426fe1560084dbb1b7))
406
+ * **mcp:** drop the agent-memory declaration ADR-094 left behind ([4283a8e](https://github.com/event4u-app/agent-config/commit/4283a8efad4ee9ed721404d3a68aff0b367ad690))
407
+ * **review:** re-bind the artefact and mark all three findings fixed ([d2a0c54](https://github.com/event4u-app/agent-config/commit/d2a0c544c80a5e20f0cd7b7d338cbf17684b7c87))
408
+ * **review:** record the R2 findings on the anti-pattern addition ([b382dd1](https://github.com/event4u-app/agent-config/commit/b382dd1bfe1b4c3a1301358eb667692e5bc606d0))
409
+ * **lint:** re-anchor the leakage allowlist after the inserted paragraph ([8bef27e](https://github.com/event4u-app/agent-config/commit/8bef27e8adae4e742350970323232981d8b499d0))
410
+ * **dist:** project the testing-anti-patterns addition ([c6fcd4a](https://github.com/event4u-app/agent-config/commit/c6fcd4ae50fbd6caaa7cc3d7b3fcf8751283815f))
411
+ * **review:** re-bind round 2 and mark its three findings fixed ([40e110c](https://github.com/event4u-app/agent-config/commit/40e110c7203b121b0e15f6ad5de0217efb1d90c8))
412
+ * **review:** record the R2 round-2 findings before fixing them ([0e477f3](https://github.com/event4u-app/agent-config/commit/0e477f357071e4bc2dc69786ce4e24c1ebf50e75))
413
+ * **review:** re-bind the R2 artefact after merging main ([756b226](https://github.com/event4u-app/agent-config/commit/756b226de64ec1a581338b07954a2e4e948b758f))
414
+ * **roadmap:** regenerate the dashboard after the main merge ([ba9a0ad](https://github.com/event4u-app/agent-config/commit/ba9a0ade70f40447b6eb3a43736b32376bad56e1))
415
+ * **review:** re-bind the R2 artefact and mark all eight findings fixed ([081ffff](https://github.com/event4u-app/agent-config/commit/081ffffbee9d0c9d28e380e148ce29f27aa5d793))
416
+ * **review:** record the R2 completion review for dispatch-safety Phase 2 ([661947d](https://github.com/event4u-app/agent-config/commit/661947def61068d05c832146723366fd5abda225))
417
+ * **config:** register shadow_floor_fire_rate as the fifth attendance metric ([70abe5c](https://github.com/event4u-app/agent-config/commit/70abe5c0118085fadd01a2eec14433814bf405c5))
418
+ * **roadmap:** close dispatch-safety 2.1-2.3 and 4.3, regenerate the dashboard ([bb8dd69](https://github.com/event4u-app/agent-config/commit/bb8dd69b31893ea235e3159ef48b5180e3385b6d))
419
+ * **dist:** project the confirmation module and the gate seam ([d6905af](https://github.com/event4u-app/agent-config/commit/d6905af2385948a7bb44dbdb3a9025834b91afb2))
420
+ * **roadmap:** close dispatch-safety Phase 2, record the 4.3 severity probe ([be0f893](https://github.com/event4u-app/agent-config/commit/be0f893e57001ca8a3a8c4c4a21538f37266f410))
421
+
422
+ Tests: 13213 (+145 since 9.34.0)
423
+
333
424
  ## [9.34.0](https://github.com/event4u-app/agent-config/compare/9.33.0...9.34.0) (2026-08-11)
334
425
 
335
426
  ### Release highlights
package/CONTRIBUTING.md CHANGED
@@ -25,20 +25,29 @@ so we can align on direction before code is written.
25
25
 
26
26
  If you are unsure whether a change is in scope: open a discussion first.
27
27
 
28
- ## Dependency direction — no circular deps with `agent-memory`
29
-
30
- `agent-config` is the upstream, standalone package. It must **never**
31
- hard-depend on `@event4u/agent-memory`. The optional companion package
32
- is declared in `suggest` (Composer) / `optionalDependencies` or
33
- documentation only (npm) — never as a runtime or dev dependency that
34
- `composer install` / `npm install` would pull automatically.
35
-
36
- Reasoning: `agent-memory` depends on `agent-config` for its skills and
37
- governance. Reversing that would create a circular dependency and break
38
- installs in consumer projects that only want the rule/skill layer.
39
-
40
- See [`agents/roadmaps/road-to-memory-self-consumption.md`](agents/roadmaps/road-to-memory-self-consumption.md)
41
- for the full conflict-resolution contract between the two packages.
28
+ ## Dependency direction — memory is file-first, with no external backend
29
+
30
+ `agent-config` is the upstream, standalone package. Its memory layer is
31
+ file-first and self-contained: curated YAML under `agents/memory/<type>/`
32
+ plus agent-written JSONL under `agents/memory/intake/`, read by `retrieve()`
33
+ in `src/scripts/memory_lookup.ts`. There is no external memory backend.
34
+
35
+ [ADR-094](docs/decisions/ADR-094-agent-memory-layer-removal.md) removed the
36
+ `@event4u/agent-memory` binding — the provider seam, the package detection,
37
+ and the MCP routing that used it "when present" — and explicitly rejected
38
+ leaving the integration surface inert: *"a dormant integration surface
39
+ advertising a capability the suite no longer has is misleading residue."*
40
+
41
+ Two rules survive that removal, and both are unconditional:
42
+
43
+ - **Never hard-depend on a package that depends on `agent-config`.** That was
44
+ the case for `agent-memory`, so the dependency could only ever run one way;
45
+ reversing it would create a cycle and break installs in consumer projects
46
+ that want the rule/skill layer alone.
47
+ - **Adding any external memory backend is a Class-B decision** under
48
+ [ADR-124](docs/decisions/ADR-124-embedded-engine-doctrine.md) § 5 — its own
49
+ ADR, a named consumer demand signal, and a measured Class-A failure — never
50
+ a dependency bump or a config entry.
42
51
 
43
52
  ## Quick start for contributors
44
53
 
@@ -42,9 +42,14 @@ git fetch origin --prune
42
42
  git ls-tree --name-only origin/main agents/roadmaps/ # still active?
43
43
  git ls-tree --name-only origin/main agents/roadmaps/archive/ # already closed?
44
44
  gh pr list --state open --json number,title,headRefName # in flight?
45
- agent-config sessions:list --json # claimed right now?
45
+ agent-config sessions:list # claimed right now?
46
46
  ```
47
47
 
48
+ `sessions:list` without `--json` on purpose: the human form carries **both**
49
+ axes — the live records AND the unmerged branches checked out in other
50
+ worktrees. `--json` returns only the records unless `--branches` is passed, and
51
+ the branch axis is the one that does not need a peer to have claimed anything.
52
+
48
53
  Excluded from the candidate set: `template.md`, `archive/`,
49
54
  `skipped/`, `later/`, anything with `status: draft` whose promotion
50
55
  trigger has not fired, and:
@@ -53,12 +58,24 @@ trigger has not fired, and:
53
58
  |---|---|---|---|
54
59
  | **taken by an open PR** | `gh pr list` — a roadmap slug matching an open PR's branch | The work is done or nearly done and is waiting on review | Review or merge that PR; the roadmap archives inside it |
55
60
  | **claimed by a live session** | `sessions:list` — a live record whose `roadmap_slug` matches | Another session is working on it *right now*, before any PR exists | Nothing to do — it releases on that session's end, or by TTL |
61
+ | **held by a foreign worktree** | `sessions:list` — an unmerged branch in another worktree whose name names this roadmap | A peer is already writing code, whether or not it ever claimed | Pick something else; a branch on disk outranks an unwritten claim |
56
62
 
57
- Keep the two distinguishable in the reported screen. They are different
63
+ Keep the three distinguishable in the reported screen. They are different
58
64
  states with different recovery, and collapsing them into "unavailable"
59
65
  loses the only information that tells the user whether to wait for a
60
66
  review or simply pick something else.
61
67
 
68
+ ```
69
+ A DIFFERENT BRANCH NAME DOES NOT MAKE IT A DIFFERENT TASK.
70
+ MEASURED TWICE: TWO SESSIONS BUILT ONE ROADMAP PHASE UNDER
71
+ `feat/dispatch-safety-confirmation` AND `feat/dispatch-safety-confirmed-execution`,
72
+ AND ONE OF THE TWO PRs WAS THROWN AWAY. COMPARE THE ROADMAP, NEVER THE BRANCH.
73
+ ```
74
+
75
+ A slug in the register that names **no open roadmap** is not a claim — it is a
76
+ stale field left behind after that roadmap archived. `sessions:list` labels it
77
+ `← STALE`; read it as "nobody claimed anything", never as "taken".
78
+
62
79
  A roadmap completes by an archive move **inside its merging PR**, so a
63
80
  local checkout can show a closed roadmap as open. That is why this step
64
81
  precedes the screen instead of following it.
@@ -145,9 +162,24 @@ agent-config sessions:claim <roadmap-slug>
145
162
  ```
146
163
 
147
164
  Immediately after the pick, before any branch or commit. This writes the slug
148
- into `agents/runtime/state/roadmap-claim.json`; the next heartbeat lifts it into
149
- this session's register record, so another session's step 1 excludes it **within
150
- one turn** and long before a PR exists.
165
+ into `agents/runtime/state/roadmap-claim-<session>.json`; the next heartbeat lifts
166
+ it into this session's register record, so another session's step 1 excludes it
167
+ **within one turn** and long before a PR exists.
168
+
169
+ The file is keyed on the host session id, not on the worktree, and that
170
+ distinction was a measured defect rather than a nicety: with one shared file per
171
+ checkout, four live records once carried one identical slug — naming an
172
+ already-archived roadmap — because every session in that checkout read the last
173
+ claim written there. A session that inherits a peer's claim reports work it is
174
+ not doing, and the peer whose claim was overwritten reports none. On a host that
175
+ exports no session id the legacy shared path is still written and still read, and
176
+ `sessions:claim` says so on that path rather than implying a guarantee it cannot
177
+ give.
178
+
179
+ **The claim is also what makes the duplicate-work warning possible.** Without it
180
+ the register can only compare branch names, and two sessions on one roadmap
181
+ routinely pick two different names — so a skipped claim does not merely delay
182
+ visibility, it removes the only axis that sees the expensive collision.
151
183
 
152
184
  Routing the claim through a state file rather than having this command write the
153
185
  register directly is deliberate: the roadmap is picked mid-session by the model,
@@ -46,6 +46,13 @@
46
46
  * ./agent-config gates --all # every open blocker, grouped by owner
47
47
  * ./agent-config gates --json # machine-readable
48
48
  * ./agent-config gates --reply # reply-close form; empty when none
49
+ * ./agent-config gates --pending # staged actions awaiting confirmation
50
+ *
51
+ * `--pending` reads a different source — the staged-confirmation store, not the
52
+ * roadmap tree — and stays out of `--reply` on purpose. ADR-222 fixes the
53
+ * reply-close form at exactly ONE decision rendered in full; folding a second
54
+ * source into it would silently change which decision "the one" is, and that
55
+ * contract is not this step's to move.
49
56
  */
50
57
 
51
58
  import * as path from 'node:path';
@@ -58,6 +65,8 @@ import {
58
65
  blocker_needs_user as needsUser,
59
66
  type Blocker,
60
67
  } from './update_roadmap_progress.js';
68
+ import { listPending } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.js';
69
+ import type { StagedAction } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation.js';
61
70
 
62
71
  const _HERE = fileURLToPath(import.meta.url);
63
72
 
@@ -327,6 +336,89 @@ function renderJson(entries: readonly Entry[], all: boolean): string {
327
336
  );
328
337
  }
329
338
 
339
+ /**
340
+ * `--pending`: staged actions awaiting a human confirmation.
341
+ *
342
+ * A second source under the same verb, and the reason it belongs here rather
343
+ * than in a new command is the verb's own definition — "open decisions that
344
+ * need you, as actions". A staged irreversible action IS one, and the most
345
+ * literal kind: the roadmap blockers above are decisions a human owes a plan,
346
+ * this is a decision a human owes an action that is already loaded and waiting.
347
+ *
348
+ * Expired stages are counted, never listed as actionable. An expired stage
349
+ * cannot execute (`stageStatus` derives that from the clock, not from a sweep
350
+ * having run), so rendering it beside a live one would put two rows in front of
351
+ * the reader where only one is a decision.
352
+ */
353
+ function renderPending(root: string, now: number): string {
354
+ const rows = listPending(root, now);
355
+ const live = rows.filter((r) => r.status === 'pending');
356
+ const expired = rows.filter((r) => r.status === 'expired');
357
+ const lines: string[] = [];
358
+
359
+ if (live.length === 0) {
360
+ lines.push(
361
+ expired.length === 0
362
+ ? 'No staged actions awaiting confirmation.'
363
+ : `Nothing awaits your confirmation. ${expired.length} stage` +
364
+ `${expired.length !== 1 ? 's' : ''} expired unconfirmed — the action never fired.`,
365
+ );
366
+ return lines.join('\n') + '\n';
367
+ }
368
+
369
+ lines.push(
370
+ `${live.length} staged action${live.length !== 1 ? 's' : ''} await` +
371
+ `${live.length === 1 ? 's' : ''} your confirmation` +
372
+ (expired.length > 0 ? ` · ${expired.length} expired unconfirmed` : ''),
373
+ );
374
+ let n = 0;
375
+ for (const { stage } of live) {
376
+ n += 1;
377
+ lines.push(...renderStage(stage, n));
378
+ }
379
+ lines.push('');
380
+ return lines.join('\n') + '\n';
381
+ }
382
+
383
+ function renderStage(stage: StagedAction, index: number): string[] {
384
+ const head = `${index} · ${stage.action}`;
385
+ const tail = `token: ${stage.token}`;
386
+ const dashes = Math.max(3, WIDTH - head.length - tail.length - 6);
387
+ return [
388
+ '',
389
+ `── ${head} ${'─'.repeat(dashes)} ${tail}`.trimEnd(),
390
+ // The object is the first field on purpose: an approval that does not
391
+ // name the exact object is the thing non-destructive-by-default forbids,
392
+ // so it is what the reader must see before anything else.
393
+ ...field('Object:', stage.object),
394
+ ...field('Staged by:', stage.source),
395
+ ...field('Expires:', stage.expires_at),
396
+ ];
397
+ }
398
+
399
+ function renderPendingJson(root: string, now: number): string {
400
+ const rows = listPending(root, now);
401
+ return (
402
+ JSON.stringify(
403
+ {
404
+ awaitingYou: rows.filter((r) => r.status === 'pending').length,
405
+ expired: rows.filter((r) => r.status === 'expired').length,
406
+ staged: rows.map(({ stage, status }) => ({
407
+ token: stage.token,
408
+ action: stage.action,
409
+ object: stage.object,
410
+ source: stage.source,
411
+ stagedAt: stage.staged_at,
412
+ expiresAt: stage.expires_at,
413
+ status,
414
+ })),
415
+ },
416
+ null,
417
+ 2,
418
+ ) + '\n'
419
+ );
420
+ }
421
+
330
422
  function _isDir(p: string): boolean {
331
423
  try {
332
424
  return fs.statSync(p).isDirectory();
@@ -340,8 +432,8 @@ function _isDir(p: string): boolean {
340
432
  * `agents/roadmaps`, otherwise fall back to the git toplevel (monorepo
341
433
  * sub-project support).
342
434
  */
343
- function _resolveRepoRoot(start: string): string {
344
- if (_isDir(path.join(start, 'agents', 'roadmaps'))) {
435
+ function _resolveRepoRoot(start: string, marker = path.join('agents', 'roadmaps')): string {
436
+ if (_isDir(path.join(start, marker))) {
345
437
  return start;
346
438
  }
347
439
  try {
@@ -351,7 +443,7 @@ function _resolveRepoRoot(start: string): string {
351
443
  timeout: 10_000,
352
444
  });
353
445
  const top = (r.stdout || '').trim();
354
- if (r.status === 0 && top !== '' && _isDir(path.join(top, 'agents', 'roadmaps'))) {
446
+ if (r.status === 0 && top !== '' && _isDir(path.join(top, marker))) {
355
447
  return top;
356
448
  }
357
449
  } catch {
@@ -365,6 +457,18 @@ function main(argv?: readonly string[]): number {
365
457
  const all = args.includes('--all');
366
458
  const json = args.includes('--json');
367
459
  const reply = args.includes('--reply');
460
+ const pending = args.includes('--pending');
461
+
462
+ // `--pending` resolves against `agents/runtime/`, not `agents/roadmaps/`,
463
+ // and is answered BEFORE the roadmaps-directory exit below: a staged action
464
+ // is independent of whether this project plans in roadmaps at all, so
465
+ // "no roadmaps here" is not an answer to "what awaits my confirmation".
466
+ if (pending) {
467
+ const root = _resolveRepoRoot(process.cwd(), path.join('agents', 'runtime'));
468
+ const now = Date.now();
469
+ process.stdout.write(json ? renderPendingJson(root, now) : renderPending(root, now));
470
+ return 0;
471
+ }
368
472
 
369
473
  const repoRoot = _resolveRepoRoot(process.cwd());
370
474
  const roadmapRoot = path.join(repoRoot, 'agents', 'roadmaps');
@@ -412,5 +516,16 @@ if (_isCliEntry()) {
412
516
  process.exitCode = main();
413
517
  }
414
518
 
415
- export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson, renderReply };
519
+ export {
520
+ main,
521
+ needsUser,
522
+ regroupTodo,
523
+ wrap,
524
+ collectEntries,
525
+ render,
526
+ renderJson,
527
+ renderReply,
528
+ renderPending,
529
+ renderPendingJson,
530
+ };
416
531
  export type { Entry };
@@ -163,6 +163,27 @@ expect([403, 404]).toContain((await api.get(`/users/${otherTenantUser.id}`)).sta
163
163
 
164
164
  Deriving from seeded data is necessary but **not sufficient** — a test that only reads back the exact fields it seeded is still overfit (a mutation in the code survives it). A real test also exercises boundary (empty / null / max / Unicode), error (missing / invalid), and, on security-sensitive paths, an abuse case (IDOR, injection string, XSS payload). Coverage of *cases*, not just lines.
165
165
 
166
+ **The adversarial variant, and it is the one that looks safest.** A *negative* test — the abuse case just named — fails the same way, but nothing about it reads as weak: it names an attack, it asserts a refusal, it goes green. It passes because **something other than the control under test** produced that refusal. Two shapes, both from one real review:
167
+
168
+ - **The hostile value sits only in the argument.** A guard rejecting a path-traversing `token` was pinned by planting a fixture whose own `token` field held the *legitimate* value while `'../outside'` was passed as the argument. Downstream, an ordinary token comparison mismatched and returned exactly the expected refusal — delete the guard and the test still passes. The hostile value belongs in the **field under test**, not only in the call.
169
+ - **The assertion cannot fail.** The same test asserted the planted file still existed after a rejected claim. The two directories were siblings, so both `../x.json` joins resolved to one path and the rename could never have moved it — a green no implementation could turn red.
170
+
171
+ Gate:
172
+
173
+ ```
174
+ BEFORE trusting a negative test:
175
+ Delete or invert the control it claims to pin. Does it FAIL?
176
+ RESTORE THE CONTROL IMMEDIATELY — before any other edit, before any commit.
177
+ The mutation is a probe, never a change. A deleted guard left deleted is
178
+ a shipped vulnerability produced by a test-quality check.
179
+ IF it still passes: it is pinning something else. Name what, then fix the test.
180
+ IF an assertion cannot fail for ANY implementation: delete THE ASSERTION —
181
+ it is decoration. Only after proving vacuity, and never to turn a red test
182
+ green — that is Anti-Pattern 7 below.
183
+ ```
184
+
185
+ No mutation-testing rig required: comment the control out, run that one spec, put it back. A negative test nobody has ever watched fail is a claim, not evidence.
186
+
166
187
  ### Anti-Pattern 7 — Gaming the green (test-integrity)
167
188
 
168
189
  Symptom: a test fails, and the "fix" edits the *test* instead of the code — `.skip` / `it.skip` / `xfail` / `@Disabled` on the failing case, deleting the failing assertion, loosening `toBe(x)` to `toBeTruthy()`, or rewriting the `expected` value to whatever the (possibly buggy) code now emits. The suite goes green while the behavior stays broken — the worst outcome, because green now *hides* the bug. This is the automation-bias trap: under pressure to finish, the agent optimizes the signal (green) instead of the target (correct behavior).
@@ -211,6 +232,7 @@ This is the test-surface instance of the `autonomous-execution` N=3 / allowlist-
211
232
  - Do NOT mark a story complete until at least one test was watched failing first.
212
233
  - Do NOT hardcode an expected value the code will emit, or pin a regex to an incidental fixed date/id — derive from inputs and seeded data, and vary the input across cases.
213
234
  - Do NOT skip / `xfail` / delete a failing test, or rewrite its `expected` to the code's current output, to get a green run — fix the code, or state a real spec change in the diff.
235
+ - Do NOT trust a negative test you have never watched fail — delete the control it pins, confirm the test goes red, restore the control immediately.
214
236
 
215
237
  ## Auto-trigger keywords
216
238
 
@@ -225,6 +247,9 @@ This is the test-surface instance of the `autonomous-execution` N=3 / allowlist-
225
247
  - gaming the green
226
248
  - skip failing test
227
249
  - test integrity
250
+ - negative test
251
+ - vacuous assertion
252
+ - abuse-case test
228
253
 
229
254
  ## Provenance
230
255
 
@@ -39,7 +39,7 @@ schema_version: 1
39
39
  # CI guard: a release bump of `package.json` must update this value
40
40
  # in lockstep — see scripts/check_template_pin_drift.ts (road-to-
41
41
  # portable-runtime-and-update-check P3.3).
42
- agent_config_version: "9.34.0"
42
+ agent_config_version: "9.35.0"
43
43
 
44
44
  # --- Project identity ---
45
45
  project:
@@ -388,8 +388,7 @@ function* _iter_knowledge_entries(): Generator<[string, Record<string, unknown>]
388
388
  /**
389
389
  * Naive relevance score: max over keys of (glob-match | substring).
390
390
  *
391
- * Good enough for the `absent` path where retrieval is best-effort.
392
- * The `present` path returns a real score from agent-memory.
391
+ * Retrieval is best-effort by design — this is the only scoring path.
393
392
  */
394
393
  export function _score(entry: Record<string, unknown>, keys: string[]): number {
395
394
  if (keys.length === 0) {
@@ -214,10 +214,8 @@ export interface SignalRecord {
214
214
  /**
215
215
  * Append a signal entry. Returns the written record, or null when skipped.
216
216
  *
217
- * On `present` backend, routing to the package is a no-op here today —
218
- * the package adapter is wired in `road-to-agent-memory-integration.md`
219
- * Phase 3. For now, the file path is the single source of truth so
220
- * merge-safety is preserved in every mode.
217
+ * The file path is the single source of truth: there is no external backend
218
+ * to route to, so merge-safety holds in every mode.
221
219
  */
222
220
  export function emit(
223
221
  entry_type: string,
@@ -41,6 +41,22 @@ type Any = unknown;
41
41
 
42
42
  const _BLOCK_REASON_PREFIX = 'decision_gate';
43
43
 
44
+ /**
45
+ * Stage an action for confirmation and return the surface lines that name it.
46
+ *
47
+ * An injected collaborator rather than a direct import of
48
+ * `staged_confirmation_store.js`, for two reasons. The gate is called from a
49
+ * hook bus with no repo root and no clock of its own, and — the load-bearing
50
+ * one — an absent stager keeps the `ask` path byte-identical to what ships
51
+ * today. The confirmation primitive is deliberately UNBOUND until blocker
52
+ * `confirmation-degraded-host-semantics` is decided (5 of 8 hosts have no
53
+ * `pre_tool_use` slot), so reaching it has to be an explicit opt-in and not a
54
+ * behaviour change that arrives with an upgrade.
55
+ */
56
+ export interface ConfirmationStager {
57
+ stage(input: { action: string; object: string; source: string }): readonly string[];
58
+ }
59
+
44
60
  /**
45
61
  * Evaluate decision-engine gates on every `AFTER_STEP`.
46
62
  *
@@ -49,9 +65,11 @@ const _BLOCK_REASON_PREFIX = 'decision_gate';
49
65
  */
50
66
  export class DecisionGateHook {
51
67
  private readonly _settings: DecisionEngineSettings;
68
+ private readonly _stager: ConfirmationStager | null;
52
69
 
53
- constructor(settings: DecisionEngineSettings) {
70
+ constructor(settings: DecisionEngineSettings, stager: ConfirmationStager | null = null) {
54
71
  this._settings = settings;
72
+ this._stager = stager;
55
73
  }
56
74
 
57
75
  /** Register the gate callback on `AFTER_STEP`. */
@@ -109,10 +127,22 @@ export class DecisionGateHook {
109
127
  DecisionGateHook._surface(decision, 'ask_timeout'),
110
128
  );
111
129
  }
112
- throw new HookHalt(
113
- `${_BLOCK_REASON_PREFIX}:${decision.gate_id}`,
114
- DecisionGateHook._surface(decision),
115
- );
130
+ // `stop` and `ask` share this exit: both halt, which is the policy —
131
+ // ask and action are strictly sequential, so an interactive gate does
132
+ // not act while it asks. What `ask` adds, and only when a stager is
133
+ // injected, is a token on the surface so the answer can be matched to
134
+ // THIS question and consumed exactly once. The halt reason is
135
+ // deliberately unchanged: it is the identity downstream readers match on.
136
+ let surface = DecisionGateHook._surface(decision);
137
+ if (action === 'ask' && this._stager !== null) {
138
+ const extra = this._stager.stage({
139
+ action: `work_engine:advance:${decision.gate_id}`,
140
+ object: `phase ${decision.phase}`,
141
+ source: _BLOCK_REASON_PREFIX,
142
+ });
143
+ surface = [...surface, '', ...extra];
144
+ }
145
+ throw new HookHalt(`${_BLOCK_REASON_PREFIX}:${decision.gate_id}`, surface);
116
146
  }
117
147
 
118
148
  // -- formatting helpers -------------------------------------------