@event4u/agent-config 14.1.0 → 14.4.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 (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +291 -181
  3. package/agents/templates/.ai-council.yml.example +35 -0
  4. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
  5. package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
  6. package/dist/agent-src/scripts/resume_probe.ts +154 -9
  7. package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
  8. package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
  9. package/dist/agent-src/templates/agent-settings.md +70 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  12. package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  13. package/dist/cli/python/workspace_drive.js +8 -3
  14. package/dist/cli/python/workspace_drive.js.map +1 -1
  15. package/dist/cli/python/workspace_hosts.js +16 -12
  16. package/dist/cli/python/workspace_hosts.js.map +1 -1
  17. package/dist/cli/registry.js +2 -0
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
  20. package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
  21. package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
  22. package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
  23. package/dist/cli-delegate/cmd_conformance.js +26 -11
  24. package/dist/cli-delegate/cmd_doctor.js +2 -2
  25. package/dist/cli-delegate/cmd_handoff.js +3 -3
  26. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  27. package/dist/cli-delegate/cmd_settings_set.js +17 -3
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +4 -4
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/dispatch.js +3773 -1731
  38. package/dist/mcp/registry-manifest.json +2 -2
  39. package/dist/mcp/server.json +2 -2
  40. package/dist/mcp/server.mjs +1070 -408
  41. package/dist/scripts/_lib/host_launch.js +66 -0
  42. package/dist/scripts/_lib/host_launch.js.map +1 -0
  43. package/dist/server/io/yamlIO.js +175 -3
  44. package/dist/server/io/yamlIO.js.map +1 -1
  45. package/dist/server/routes/wizard.js +20 -1
  46. package/dist/server/routes/wizard.js.map +1 -1
  47. package/dist/shared/settingsConsent.js +25 -0
  48. package/dist/shared/settingsConsent.js.map +1 -1
  49. package/docs/contracts/settings-classes.md +11 -0
  50. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
  51. package/package.json +1 -1
  52. package/src/agent-src/scripts/resume_probe.ts +154 -9
  53. package/src/agent-src/scripts/roadmap_gates.ts +210 -6
  54. package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  55. package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  56. package/src/config/estate-count-budget.json +120 -0
  57. package/src/config/evaluator-budgets.json +4 -4
  58. package/src/config/gate-coverage.yml +32 -4
  59. package/src/config/gate-violation-baselines.json +63 -63
  60. package/src/config/gitignore-block.txt +5 -0
  61. package/src/config/hook-latency-budget.json +40 -1
  62. package/src/config/hook-token-budget.json +22 -0
  63. package/src/config/rule-stub-ceilings.json +11 -3
  64. package/src/scripts/_dispatch.bash +42 -1
  65. package/src/scripts/_lib/council_fallback_posture.ts +81 -0
  66. package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
  67. package/src/scripts/_lib/council_settings_block.ts +152 -0
  68. package/src/scripts/_lib/headless_invocation.ts +285 -0
  69. package/src/scripts/_lib/host_launch.ts +72 -0
  70. package/src/scripts/_lib/map_to_object.ts +16 -0
  71. package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
  72. package/src/scripts/_lib/run_checkpoint.ts +409 -0
  73. package/src/scripts/_lib/session_register.ts +32 -1
  74. package/src/scripts/_lib/skill_catalogue_series.ts +91 -0
  75. package/src/scripts/_lib/stdin.ts +35 -1
  76. package/src/scripts/_lib/turn_end_refusals.ts +12 -4
  77. package/src/scripts/_lib/unattended_guard.ts +371 -0
  78. package/src/scripts/ai_council/config.ts +27 -1
  79. package/src/scripts/ai_council/events_log.ts +12 -1
  80. package/src/scripts/ai_council/fallback_config.ts +114 -0
  81. package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
  82. package/src/scripts/ai_council/orchestrator.ts +251 -292
  83. package/src/scripts/ai_council/py_parity.ts +139 -0
  84. package/src/scripts/ai_council/response_render.ts +99 -0
  85. package/src/scripts/ai_council/spend_gate.ts +123 -0
  86. package/src/scripts/ai_council/transport_resolver.ts +81 -13
  87. package/src/scripts/bench_hook_injection.ts +100 -6
  88. package/src/scripts/bench_hook_latency.ts +442 -16
  89. package/src/scripts/check_estate_count.ts +924 -0
  90. package/src/scripts/compile_hook_manifest.ts +67 -0
  91. package/src/scripts/council_cli.ts +100 -125
  92. package/src/scripts/decision_memo.ts +325 -0
  93. package/src/scripts/dispatch_r2_reviewer.ts +169 -15
  94. package/src/scripts/hook_manifest.json +1 -0
  95. package/src/scripts/hook_manifest.yaml +172 -25
  96. package/src/scripts/hooks/concern_registry.ts +6 -0
  97. package/src/scripts/hooks/dispatch_hook.ts +183 -10
  98. package/src/scripts/hooks/dispatch_issues.ts +26 -0
  99. package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
  100. package/src/scripts/hooks/hook_stdin.ts +111 -5
  101. package/src/scripts/hooks/injection_budget.ts +595 -0
  102. package/src/scripts/hooks/payload_stub.ts +446 -0
  103. package/src/scripts/hooks/run_continuation_hook.ts +627 -0
  104. package/src/scripts/hooks/session_eol_hook.ts +52 -0
  105. package/src/scripts/hooks/skill_route_hook.ts +172 -11
  106. package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
  107. package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
  108. package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
  109. package/src/scripts/hooks_doctor.ts +16 -2
  110. package/src/scripts/install-hooks.sh +26 -0
  111. package/src/scripts/interruption_report.ts +330 -13
  112. package/src/scripts/lint_hook_manifest.ts +167 -0
  113. package/src/scripts/roadmap_progress_hook.ts +254 -46
  114. package/src/scripts/run_supervise.ts +623 -0
  115. package/src/scripts/session_register_hook.ts +108 -8
  116. package/src/scripts/sessions_cli.ts +18 -6
  117. package/src/scripts/telemetry_disclosure_hook.ts +244 -0
  118. package/src/server/io/yamlIO.ts +167 -3
  119. package/src/shared/settingsConsent.ts +49 -2
  120. package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
@@ -207,6 +207,23 @@ cli_call_budget:
207
207
  # Warn once usage crosses this fraction of a cap (0..1).
208
208
  warn_at: 0.8
209
209
 
210
+ # Mid-flight cli -> api fallback. When a CLI call fails partway through a
211
+ # pass, the seat can retry once on that provider's metered api rung instead
212
+ # of being lost. The classes where the CLI provably never reached the
213
+ # provider (missing binary, rejected auth, unsupported CLI, unservable
214
+ # model) fall through unconditionally; a timeout or a 5xx NEVER does, under
215
+ # any setting, because a half-completed call must not be paid for twice.
216
+ #
217
+ # `api_on_quota` is the one BILLING-CLASS decision in that design, which is
218
+ # why it is a key and not a rule. Both quota shapes are safe to retry — the
219
+ # local `cli_call_budget` cap refuses before anything is sent, and a
220
+ # provider plan quota is rejected at the request boundary — so the question
221
+ # is not double spend, it is that an unmetered subscription call becomes a
222
+ # USD one. Off by default; the retry still passes the `cost_budget` gates
223
+ # above. Turn it on if you would rather keep the seat than keep the $0.
224
+ fallback:
225
+ api_on_quota: false
226
+
210
227
  # Quorum for a council pass to CONCLUDE (road-to-always-on-orchestration
211
228
  # Phase 3.3). Default "majority" resolves to ceil(n / 2) enabled members —
212
229
  # a SIMPLE majority, deliberately not "more than half": at n=2 that is
@@ -499,6 +516,23 @@ necessity_classifier:
499
516
  #
500
517
  # See docs/contracts/ai-council-config.md for the trigger lists and the
501
518
  # Iron-Law clause.
519
+ # `second_model` — the OPTIONAL local rung between the agent and the
520
+ # council (UOTL Phase 4.1). It exists because the council used to be the
521
+ # only rung before a halt: a medium-impact question stopped the run
522
+ # whenever no council was configured or its quota was gone.
523
+ #
524
+ # Name a provider whose LOCAL CLI runs under subscription auth —
525
+ # anthropic, openai or gemini. That set is narrower than the five
526
+ # providers `members:` accepts, on purpose: xai and perplexity ship
527
+ # community CLI wrappers that consume an API key, so routing a
528
+ # resolution there would spend USD on the rung whose whole point is not
529
+ # to. The rung MUST book against the same `cli_call_budget` counter above
530
+ # — one subscription, one counter. That is an obligation on the rung, not
531
+ # a guard that already exists: nothing reads this key at runtime yet, so
532
+ # setting it declares intent rather than activating a bounded mechanism.
533
+ #
534
+ # The key is REFUSED on high_impact and user_required, not ignored. Those
535
+ # classes reach you, and a silently-dropped key would read as configured.
502
536
  decision_resolution:
503
537
  enabled: true
504
538
  classes:
@@ -511,6 +545,7 @@ decision_resolution:
511
545
  medium_impact:
512
546
  mode: council
513
547
  confidence_threshold: 0.6
548
+ # second_model: gemini # uncomment to add the local rung
514
549
  high_impact:
515
550
  mode: user # LOCKED — Iron Law
516
551
  confidence_threshold: 0.6
@@ -277,11 +277,39 @@ For each open step in the working set (scope-bound — see wrapper):
277
277
  summary named the members). `high_impact` / `user_required`
278
278
  classifications still escalate to the user per
279
279
  [`ask-when-uncertain`](../../rules/ask-when-uncertain.md).
280
+ - **Second-model rung, when the class declares one**
281
+ (`decision_resolution.classes.<cls>.second_model`, UOTL Phase 4.1)
282
+ → one local vendor-CLI pass under subscription auth. **The rung is
283
+ MODEL-CARRIED: the key is declared and schema-validated, and no
284
+ TypeScript path reads it at runtime** — so "bounded by the
285
+ `cli_call_budget` counter" is what the rung must respect, never
286
+ something a booking consumer enforces (`cli_call_budget.ts` declares
287
+ its consumer set closed at two, and this is not one of them). Stated
288
+ rather than implied after R2 round 4 finding 4 found the coupling
289
+ asserted on three surfaces at once. Available
290
+ to `trivial` / `low_impact` / `medium_impact` only; the config
291
+ schema REFUSES the key on `high_impact` and `user_required`, so no
292
+ locked question can reach it.
280
293
  - **Council off / not configured** → halt, surface once, wait.
281
294
  Resume on next turn. An execution contract cannot enable a
282
295
  council that has no configured members — the contract summary
283
296
  says so upfront, and in-run ambiguity halts (never silent
284
297
  guessing).
298
+ - **No self-adversarial fallback.** With neither a council nor a
299
+ second-model rung available, the ambiguity halt STANDS. The gap is
300
+ never filled by the agent arguing both sides of the question to
301
+ itself: a monologue produces a verdict with no independent
302
+ observer, which is the failure
303
+ [`evaluator-independence`](../../rules/evaluator-independence.md)
304
+ exists over, and reads as convergence to whoever finds it later.
305
+ Halting costs one turn; a manufactured verdict costs the trust in
306
+ every other verdict the run produced.
307
+ - **A resolution taken WITHOUT contacting the user is recorded** —
308
+ `agent-config decision:memo write --run <id> …` (question, chosen
309
+ option, reasoning, resolver, confidence). The run's PR description
310
+ links the directory. Not a gate: the memo is what makes an
311
+ autonomous resolution reviewable after the fact, which is the
312
+ condition under which it is legitimate at all.
285
313
  5. **Atomic flip — same reply, every step.**
286
314
  Flip the checkbox in `agents/roadmaps/<file>.md`: `[x]` done ·
287
315
  `[~]` partial · `[-]` skipped. **Non-skippable, non-batchable**
@@ -345,20 +373,56 @@ For each open step in the working set (scope-bound — see wrapper):
345
373
  file is the source of truth between regens.
346
374
  7. Run quality pipeline if cadence is `per_step`.
347
375
 
376
+ ### 5d. Resuming a run whose previous session died
377
+
378
+ A session above the recycle threshold and inside a running contract leaves a
379
+ **deterministic checkpoint** at
380
+ `agents/runtime/state/checkpoints/<run>.json` — open / done / parked counts,
381
+ the next open step, and the commit the tree was on. Every field is recomputed
382
+ from the roadmap on disk rather than summarised, which is what makes the next
383
+ line possible.
384
+
385
+ ```
386
+ A RESUMED RUN RE-VERIFIES THE CHECKPOINT BEFORE ACTING ON IT.
387
+ RESUME BY EVIDENCE, NEVER BY BOOKKEEPING.
388
+ ```
389
+
390
+ The first act of a resumed run is to look its checkpoint up **by roadmap slug**
391
+ — `latestCheckpointFor(repoRoot, slug)` — and then `verifyCheckpoint` it against
392
+ the current tree. The slug, not the run id: a relaunched session has a NEW
393
+ session id, so the run-id-keyed `readCheckpoint` cannot find the checkpoint the
394
+ DYING session wrote, and this instruction named an unreachable path until the
395
+ R2 review's finding 7. The slug is the one key a resumed run holds by
396
+ definition, because claiming the same roadmap is what makes it a resume.
397
+
398
+ The per-field report then names WHICH claim went stale, which is what a bare
399
+ "stale/fresh" verdict cannot. **A disagreement is not an error**: work landing
400
+ between the checkpoint and the resume is the normal case (a human committed, a
401
+ sibling worktree moved, the dying session finished a step it never recorded),
402
+ and the `actual` column is what to resume from. Treating progress as corruption
403
+ would refuse every healthy resume.
404
+
405
+ `agent-config run:supervise --once` reports which runs died with open steps
406
+ left. It never merges, pushes, or closes anything — that boundary is a named
407
+ rejection, not a missing feature.
408
+
348
409
  ### Halt conditions
349
410
 
350
411
  - Hard-Floor trigger ([`non-destructive-by-default`](../../rules/non-destructive-by-default.md))
351
412
  - Security-sensitive path ([`security-sensitive-stop`](../../rules/security-sensitive-stop.md))
352
413
  - Step reveals work outside the roadmap's scope
353
414
  - Test failure or quality red on `per_step`
354
- - Council off + true ambiguity — under an **accepted execution
355
- contract** ([§ 3](#3-pre-scan--execution-contract-or-commit-step-ask))
356
- this halt exists only when no council is configured: the contract
357
- auto-enables council for the run, so in-run open questions resolve
358
- silently (`high_impact` / `user_required` classifications still
359
- escalate per [`ask-when-uncertain`](../../rules/ask-when-uncertain.md));
360
- with no council configured, true ambiguity halts — never silent
361
- guessing.
415
+ - Council off, no second-model rung, and true ambiguity — under an
416
+ **accepted execution contract**
417
+ ([§ 3](#3-pre-scan--execution-contract-or-commit-step-ask)) this halt
418
+ exists only when neither rung is available: the contract auto-enables
419
+ council for the run, so in-run open questions resolve silently
420
+ (`high_impact` / `user_required` classifications still escalate per
421
+ [`ask-when-uncertain`](../../rules/ask-when-uncertain.md)); with a
422
+ class-declared `second_model` the local rung runs first. With neither,
423
+ true ambiguity halts — never silent guessing, and never a
424
+ self-adversarial monologue in place of the missing observer (the
425
+ open-question handling in § 5 states that rung by rung).
362
426
 
363
427
  An accepted execution contract **never lifts a Hard Floor** or any of
364
428
  the other halts above — it removes redundant *asks* (git shape,
@@ -438,10 +502,17 @@ The loop MUST:
438
502
  five choices: follow-up (draft), follow-up (ready + blocked),
439
503
  keep-in-archive (intentional drop), restore to `[ ]`, convert
440
504
  to `[-]` cancelled.
441
- 3. Wait for the user. The autonomous mandate (`/work`,
442
- `/roadmap:process-full`, "decide for me") does **not** lift this
443
- gate — Iron Law 3 calls it "the canonical lost-information failure
444
- mode this rule exists to prevent."
505
+ 3. **Route by the preservation test**, per
506
+ [`roadmap-progress-sync § Who resolves it`](../../rules/roadmap-progress-sync.md):
507
+ a disposition that keeps the criterion ALIVE in the active estate
508
+ (fix now · carry item + blocker into a follow-up created in the SAME
509
+ change · merge into existing active work · restore to `[ ]`) may be
510
+ resolved by the council, recorded at the item. A disposition that
511
+ drops, weakens or permanently accepts the loss of it — cancel to
512
+ `[-]`, keep-in-archive, scope cut — reaches the **user**, and the
513
+ autonomous mandate (`/work`, `/roadmap:process-full`, "decide for
514
+ me") does **not** lift that half. In doubt: user. A council verdict
515
+ naming a follow-up that does not exist yet fails closed to the user.
445
516
  4. On picks 1 / 2 → run the "Spawn follow-up from deferred items"
446
517
  procedure in [`roadmap-management`](../../skills/roadmap-management/SKILL.md).
447
518
  On picks 3 / 4 / 5 → apply the change, re-evaluate the decision
@@ -46,12 +46,56 @@ IS A RULE VIOLATION, NOT AN OVERSIGHT.
46
46
 
47
47
  ```
48
48
  A ROADMAP WITH `[~]` DEFERRED ITEMS NEVER AUTO-ARCHIVES SILENTLY.
49
- SURFACE EVERY DEFERRED STEP. ASK THE USER WHAT HAPPENS TO THE PLAN.
49
+ SURFACE EVERY DEFERRED STEP. RESOLVE IT — VIA THE COUNCIL WHERE THE
50
+ DISPOSITION PRESERVES THE ITEM, VIA THE USER WHERE IT DOES NOT.
50
51
  A SILENT ARCHIVE THAT BURIES PLANNED-FOR-LATER WORK
51
52
  IS A RULE VIOLATION, NOT A CONVENIENCE.
52
53
  ```
53
54
 
54
- Closure check fires (`count_open == 0` and `count_deferred > 0`) → enumerate every `[~]` step, present the numbered-options resolution menu (per [`user-interaction`](user-interaction.md)), and only after the user resolves the deferrals does the `git mv` to `archive/` run. Full option menu + migration mechanics: guideline + [`roadmap-management`](../skills/roadmap-management/SKILL.md).
55
+ Closure check fires (`count_open == 0` and `count_deferred > 0`) → enumerate every `[~]` step, present the numbered-options resolution menu (per [`user-interaction`](user-interaction.md)), and only after the deferrals are resolved does the `git mv` to `archive/` run. Full option menu + migration mechanics: guideline + [`roadmap-management`](../skills/roadmap-management/SKILL.md).
56
+
57
+ ### Who resolves it — the preservation test
58
+
59
+ ```
60
+ THE COUNCIL MAY RESOLVE A `[~]` ONLY WHERE THE CHOSEN DISPOSITION KEEPS THE
61
+ ITEM ALIVE IN THE ACTIVE ESTATE. ANYTHING THAT DROPS, WEAKENS, OR PERMANENTLY
62
+ ACCEPTS THE LOSS OF IT REACHES THE USER — ALWAYS, AND NO MANDATE LIFTS THAT.
63
+ IN DOUBT, IT IS A USER DECISION. THE COUNCIL ADVISES ON HOW AND WHEN;
64
+ THE OWNER DECIDES WHETHER.
65
+ ```
66
+
67
+ One question decides the route, and it is answerable from the option itself
68
+ rather than from judgement: **does this disposition keep the criterion active
69
+ in the estate?**
70
+
71
+ | Disposition | Route |
72
+ |---|---|
73
+ | Fix the blocker now, in this change or its own PR | council |
74
+ | Carry item **and** blocker into a named follow-up roadmap created in the SAME change and estate-ratchet compliant | council |
75
+ | Merge the item into existing active work that already covers it | council |
76
+ | Restore to `[ ]` in this roadmap | council |
77
+ | Convert to `[-]` cancelled | **user** |
78
+ | Weaken the criterion, cut its scope, or accept the breakage permanently | **user** |
79
+ | Keep-in-archive (an intentional drop) | **user** |
80
+ | The item carries a `high_impact` / `user_required` classification | **user** |
81
+
82
+ - **"Immediately active" is not a promise.** A follow-up counts only if created
83
+ in the same change and estate-ratchet compliant. A verdict naming a roadmap
84
+ that does not exist yet **fails closed to the user**.
85
+ - **Recorded, or it did not happen.** At the item: criterion verbatim ·
86
+ blocker id · every option · verdict + one-sentence rationale · dissent ·
87
+ destination when carried · what closes it. A verdict with no record is a
88
+ silent drop wearing a procedure.
89
+ - **The residual hole, stated not papered over.** Both authoring seats named
90
+ it in their own strongest counter: a carried follow-up can still become an
91
+ indefinite deferral, so this test bounds *who decides*, not *whether the work
92
+ happens*. Only fix-now discharges it. A carried item untouched at the next
93
+ task boundary is raised again per [`active-remediation`](active-remediation.md).
94
+ - **Why it changed.** Adopted 2026-08-19, unanimous 2/2 council (blind peer
95
+ review). The prior text — *"Wait for the user"* — handed back a fully
96
+ analysed choice with four costed options, the low-value interruption
97
+ [`no-cheap-questions`](no-cheap-questions.md) forbids. The gate protects the
98
+ item, not the maintainer's attention.
55
99
 
56
100
  ## Later disposition — blocked-for-later roadmaps are parked, never left active
57
101
 
@@ -95,9 +139,9 @@ Before sending any reply that landed roadmap work:
95
139
  4. Did `count_open` reach 0?
96
140
  - **No (real open work remains)** → continue normally.
97
141
  - **Yes + `count_deferred == 0`** → the roadmap is **complete**. Archive it — `git mv` to `archive/` + migrate inbound refs + regen, same reply — or let the next `/create-pr` § 1c sweep do it deterministically. Either way it must never be pushed to the trunk unarchived (§ PR-gate; the `--check` backstop enforces it).
98
- - **Yes + `count_deferred > 0`** → STOP. Run the Iron Law 3 deferred-resolution flow (surface items + numbered options + wait). Archive only after resolution.
142
+ - **Yes + `count_deferred > 0`** → STOP. Run the Iron Law 3 deferred-resolution flow: surface every item, apply the preservation test above, and resolve — council where the disposition keeps the item alive, user where it does not. Archive only after resolution, and only with the resolution recorded at the item.
99
143
 
100
- Any "no" at step 2 → reply is incomplete. Do not send. A skipped step 3 regen is fine when cadence permits — checkbox truth lives in the markdown file. Skipping the deferred-resolution gate at step 4 is **never** acceptable; it is the canonical "lost-information" failure mode this rule exists to prevent.
144
+ Any "no" at step 2 → reply is incomplete. Do not send. A skipped step 3 regen is fine when cadence permits — checkbox truth lives in the markdown file. Skipping the deferred-resolution gate at step 4 is **never** acceptable; it is the canonical "lost-information" failure mode this rule exists to prevent. Note what the council path does and does not change here: it changes WHO resolves a preserving disposition, never whether the gate runs, never whether the resolution is recorded, and never the user's ownership of a drop.
101
145
 
102
146
  Body migrated to `guideline:agent-infra/roadmap-progress-mechanics` (per P4 of `road-to-kernel-and-router.md`) — glyph semantics, regen cadence, deferred-resolution menu, later-disposition procedure, PR-gate prose, plus the long-form failure-mode catalog, Copilot fallback, and hook + CI defence-in-depth.
103
147
  Trigger-set above activates this routing on demand, independent of the discipline profile (ADR-110).
@@ -16,14 +16,19 @@
16
16
  *
17
17
  * ## What it can and cannot decide, stated plainly
18
18
  *
19
- * A resume condition is prose. Two shapes are machine-decidable and the rest
20
- * are not, so the verdict set has three values rather than two:
19
+ * A resume condition is prose. TWO shapes are machine-decidable — a roadmap
20
+ * reference, and a single repo-relative file path under an existence predicate
21
+ * — and the rest are not, so the verdict set has three values rather than two:
21
22
  *
22
23
  * - `fired` — every roadmap the condition names has archived, or the
23
- * named step in a still-active roadmap is ticked.
24
+ * named step in a still-active roadmap is ticked; or the
25
+ * single path it names under `exists` is there.
24
26
  * - `unmet` — at least one named roadmap is still active with the
25
- * named step (or the whole file) still open.
26
- * - `undecidable` — the condition names no roadmap this tree can resolve.
27
+ * named step (or the whole file) still open; or the single
28
+ * path it names under `exists` is absent.
29
+ * - `undecidable` — the condition names neither, names both (a conjunction
30
+ * this probe can only half-weigh), names two of either, or
31
+ * states a predicate other than existence over its path.
27
32
  *
28
33
  * `undecidable` is reported as its own count and never folded into "nothing
29
34
  * fired". A probe that reads 44 files, understands 12 of them and announces
@@ -84,6 +89,31 @@ const COMPOUND_RE = /\bboth\b|\band\b|\([1-9]\)|\([a-e]\)|`[ \t]*\+|\+[ \t]*`/i;
84
89
  const FIELD_LABEL_RE = /\*\*[^*]+:\*\*/;
85
90
  /** A roadmap slug as written in prose, with or without the `.md` suffix. */
86
91
  const ROADMAP_REF_RE = /\broad-to-[a-z0-9][a-z0-9-]*\b/g;
92
+
93
+ /**
94
+ * A repo-relative path named inside backticks — the second decidable form.
95
+ *
96
+ * `PARK-PROBEABLE` (`road-to-estate-drawdown` Phase 2) means the resume
97
+ * condition can be tested by a script, and until this existed the probe could
98
+ * decide exactly one phrasing: a roadmap slug. Measured 2026-08-19 on the live
99
+ * estate, 42 of 44 park notes were `undecidable`, and the batch-1 sweep could
100
+ * only add six more — a verdict whose name promised a probe that did not read
101
+ * the conditions it was producing.
102
+ *
103
+ * Deliberately narrow in three ways, because a false FIRED un-parks live work:
104
+ * the path must be **inside backticks** (prose that merely contains a slash is
105
+ * not a condition), it must start at a **known top-level repo directory** and
106
+ * end in a **file extension**, and the clause must name **exactly one** — two
107
+ * paths are a conjunction this function cannot weigh, and `COMPOUND_RE` does
108
+ * not catch a comma.
109
+ *
110
+ * The extension requirement is what keeps a *directory* out. `agents/evidence/`
111
+ * exists in every checkout, so "recorded under `agents/evidence/`" is not a
112
+ * condition a probe can decide — and since `_exists` tests `isFile()`, matching
113
+ * it would have reported "does not exist" about a directory that does. A
114
+ * directory-shaped condition stays `undecidable`, which is the honest verdict.
115
+ */
116
+ const REPO_PATH_RE = /`((?:agents|docs|src|tests|internal|scripts|evals|\.github)\/[A-Za-z0-9._\-/]*[A-Za-z0-9_\-]\.[A-Za-z0-9]{2,5})`/g;
87
117
  /**
88
118
  * A step reference: `P2.1`, `Phase 2.1`, or a bare `2.1`.
89
119
  *
@@ -231,6 +261,37 @@ function _blankFencedCode(text: string): string {
231
261
  );
232
262
  }
233
263
 
264
+ /**
265
+ * The head of `s` up to its first sentence terminator OUTSIDE backticks.
266
+ *
267
+ * A naive character-class scan that stops at any dot cuts inside `foo.md` or
268
+ * inside a step id like `3.3`, which truncates the clause mid-span and hides
269
+ * the very path or step the condition names. Measured 2026-08-19: three of the six park notes written by
270
+ * `road-to-estate-drawdown` Phase 2 batch 1 lost their backticked path this
271
+ * way, and the probe then reported "names no roadmap this tree can resolve"
272
+ * about a condition that named a file.
273
+ */
274
+ function _sentenceHead(s: string): string {
275
+ // An ODD backtick count means the span never closes, and toggling on it
276
+ // would leave `inCode` stuck true for the whole remainder — the commentary
277
+ // paragraph then becomes the clause, and a path mentioned only in that
278
+ // commentary could decide the verdict. Fall back to the plain scan, which
279
+ // cuts at the first terminator and is the conservative direction.
280
+ const balanced = (s.match(/`/g) ?? []).length % 2 === 0;
281
+ let inCode = false;
282
+ for (let i = 0; i < s.length; i++) {
283
+ const ch = s[i] as string;
284
+ if (ch === '`' && balanced) {
285
+ inCode = !inCode;
286
+ continue;
287
+ }
288
+ if (!inCode && (ch === '.' || ch === '—' || ch === '–')) {
289
+ return s.slice(0, i);
290
+ }
291
+ }
292
+ return s;
293
+ }
294
+
234
295
  /**
235
296
  * The condition CLAUSE inside the condition text.
236
297
  *
@@ -260,13 +321,63 @@ function conditionClause(condition: string): string {
260
321
  // (`**Resume when** that pruning lands`), so take the span plus the
261
322
  // remainder of its sentence — up to an em dash or a full stop.
262
323
  const after = condition.slice(m.index + m[0].length);
263
- const tail = /^[^—–.]*/.exec(after)?.[0] ?? '';
264
- return `${span}${tail}`;
324
+ return `${span}${_sentenceHead(after)}`;
265
325
  }
266
326
  }
267
327
  return condition;
268
328
  }
269
329
 
330
+ /**
331
+ * The clause's predicate is EXISTENCE, not some property of the file's content.
332
+ *
333
+ * Without this the path branch would answer a question the condition did not
334
+ * ask. Measured 2026-08-19 while writing it: `road-to-catalogue-host-fit` waits
335
+ * until `agents/evidence/metrics/skill-catalogue.jsonl` *holds at least 20
336
+ * observations*; the file exists today with 7 lines, so an existence test would
337
+ * have reported FIRED and un-parked a roadmap whose bar is 13 observations away.
338
+ * A content bar is decidable — by `capture_skill_catalogue --cadence` — but not
339
+ * by this probe, and `undecidable` is the honest verdict for it.
340
+ *
341
+ * The predicate is bound POSITIONALLY to the path rather than tested as a word
342
+ * anywhere in the clause, because word-presence is not a predicate: `\`x.md\` no
343
+ * longer exists` would have reported FIRED while the file was still there, and
344
+ * `blocked until a workaround exists, see \`docs/x.md\`` would have paired the
345
+ * word with a path it does not govern. Both shapes are regression tests.
346
+ *
347
+ * KNOWN LIMIT, stated rather than papered over: this probe reads the condition
348
+ * AFTER `_truncateAtNextField`, so a content bar an author moves into a sibling
349
+ * bolded field is invisible to it and the note decides as bare existence. That
350
+ * is an authoring bypass no check here can see — the guard is that a resume
351
+ * condition states its whole bar inside the condition, and it is model-carried.
352
+ */
353
+ const EXISTENCE_PREDICATE_RE = /`(?:agents|docs|src|tests|internal|scripts|evals|\.github)\/[A-Za-z0-9._\-/]+`[ \t]*(?:currently[ \t]+)?exists\b/i;
354
+
355
+ /**
356
+ * The single repo-relative path a condition names, or `null`.
357
+ *
358
+ * `null` for zero paths (nothing to test) and for two or more (a conjunction
359
+ * this probe cannot weigh — reporting FIRED on the first would be the
360
+ * partial-match resume the compound guard already refuses).
361
+ */
362
+ function referencedPath(condition: string): string | null {
363
+ if (!EXISTENCE_PREDICATE_RE.test(condition)) {
364
+ return null;
365
+ }
366
+ const paths = _pathsIn(condition);
367
+ return paths.length === 1 ? (paths[0] as string) : null;
368
+ }
369
+
370
+ /** Every distinct repo-relative file path a clause names, predicate or not. */
371
+ function _pathsIn(condition: string): string[] {
372
+ REPO_PATH_RE.lastIndex = 0;
373
+ const found = new Set<string>();
374
+ let m: RegExpExecArray | null;
375
+ while ((m = REPO_PATH_RE.exec(condition)) !== null) {
376
+ found.add(m[1] as string);
377
+ }
378
+ return [...found].sort();
379
+ }
380
+
270
381
  /** Roadmap slugs named by the condition, minus the parked file's own slug. */
271
382
  function referencedRoadmaps(condition: string, ownSlug: string): string[] {
272
383
  const out = new Set<string>();
@@ -310,7 +421,10 @@ function stepIsDone(text: string, stepId: string): boolean | null {
310
421
  * tree rather than the live estate — a probe whose only test is the live tree
311
422
  * stops testing anything the day the live tree changes.
312
423
  */
313
- function probeLater(roadmapRoot: string): ResumeFinding[] {
424
+ function probeLater(roadmapRoot: string, repoRootArg?: string): ResumeFinding[] {
425
+ // Defaults to the repo that owns `agents/roadmaps/`. Passed explicitly by
426
+ // the tests, whose fixture tree IS the roadmap root and has no repo above it.
427
+ const repoRoot = repoRootArg ?? path.resolve(roadmapRoot, '..', '..');
314
428
  const laterDir = path.join(roadmapRoot, 'later');
315
429
  let entries: string[];
316
430
  try {
@@ -353,13 +467,43 @@ function probeLater(roadmapRoot: string): ResumeFinding[] {
353
467
  continue;
354
468
  }
355
469
  const refs = referencedRoadmaps(clause, ownSlug);
470
+ // A clause naming a roadmap AND a path is a conjunction, and deciding it
471
+ // on the roadmap half alone is the partial-match resume `COMPOUND_RE`
472
+ // already refuses for two roadmaps. `COMPOUND_RE` does not close it —
473
+ // a comma joins the two without an `and`, which is the same hole the
474
+ // two-paths rule in `referencedPath` exists for. Refused symmetrically.
475
+ if (refs.length > 0 && _pathsIn(clause).length > 0) {
476
+ findings.push({
477
+ file: `later/${name}`,
478
+ condition,
479
+ refs,
480
+ verdict: 'undecidable',
481
+ why: 'names a roadmap and a repo path — a conjunction the probe can only half-weigh',
482
+ });
483
+ continue;
484
+ }
356
485
  if (refs.length === 0) {
486
+ // Second decidable form: a single repo-relative path. `exists` is
487
+ // the whole test — the condition says the artefact will appear, so
488
+ // its appearance IS the firing, and no content check is implied.
489
+ const rel = referencedPath(clause);
490
+ if (rel !== null) {
491
+ const there = _exists(path.join(repoRoot, rel));
492
+ findings.push({
493
+ file: `later/${name}`,
494
+ condition,
495
+ refs,
496
+ verdict: there ? 'fired' : 'unmet',
497
+ why: there ? `${rel} exists` : `${rel} does not exist`,
498
+ });
499
+ continue;
500
+ }
357
501
  findings.push({
358
502
  file: `later/${name}`,
359
503
  condition,
360
504
  refs,
361
505
  verdict: 'undecidable',
362
- why: 'the condition names no roadmap this tree can resolve',
506
+ why: 'the condition names no roadmap or repo path this tree can resolve',
363
507
  });
364
508
  continue;
365
509
  }
@@ -414,6 +558,7 @@ export {
414
558
  conditionClause,
415
559
  extractCondition,
416
560
  probeLater,
561
+ referencedPath,
417
562
  referencedRoadmaps,
418
563
  roadmapDisposition,
419
564
  stepIsDone,