@event4u/agent-config 14.21.0 → 14.23.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 (235) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +164 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The standing-payload bounds are shrink-only, checked against the base ref.
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * `src/config/preamble-payload-budget.json` says of its grace ceiling: *"It may
7
+ * never move UP."* `grace_ceiling_history` in the same file records it moving up
8
+ * twice — 138,212 → 138,273 on 2026-09-02 and 138,273 → 138,490 on 2026-09-08 —
9
+ * each with a careful prose justification. ADR-264 resolved that contradiction
10
+ * against the practice: the sentence stands, the two raises are violations caused
11
+ * by contradictory wording, and standing-rule growth needs a compensating
12
+ * reduction elsewhere.
13
+ *
14
+ * That decision then had no enforcement. A sentence in a JSON string is not a
15
+ * gate, and the file's own history is the evidence: nothing objected either time.
16
+ * This module is the enforcement — and it is the ONLY thing
17
+ * `road-to-a-standing-budget-with-headroom` step 1.3 ships, because the 128-token
18
+ * Iron Law reserve that step designed was refused by an AI council 2/2 on
19
+ * 2026-09-08. See ADR-265.
20
+ *
21
+ * WHAT IT CANNOT DO, STATED PLAINLY
22
+ * ---------------------------------
23
+ * The change under review can edit this file. Both council seats named that as
24
+ * the defect fatal to the reserve — "the checker, its imports, token census,
25
+ * workflow, and status context are all part of the trusted computing base", so a
26
+ * protected record read by an unprotected verifier grants nothing. It is fatal to
27
+ * a mechanism that GRANTS budget and it is not fatal to one that only REFUSES,
28
+ * which is the whole reason the reserve stayed unshipped and this ratchet did
29
+ * not: disabling this check requires visibly editing it, and every gate in this
30
+ * repository already rests on that same review. A ratchet whose only failure mode
31
+ * is "someone deleted the ratchet" is strictly better than the sentence it
32
+ * replaces, which had no failure mode because it did nothing.
33
+ */
34
+ import { spawnSync } from 'node:child_process';
35
+
36
+ /** Repo-relative path of the budget config the bound lives in. */
37
+ export const BUDGET_CONFIG_PATH = 'src/config/preamble-payload-budget.json';
38
+
39
+ /** Injection seam: run git, capture output, never throw on a non-zero exit. */
40
+ export type GitRunner = (args: readonly string[], cwd: string) => {
41
+ ok: boolean;
42
+ stdout: string;
43
+ stderr: string;
44
+ };
45
+
46
+ export const realGit: GitRunner = (args, cwd) => {
47
+ const r = spawnSync('git', [...args], { cwd, encoding: 'utf-8' });
48
+ return { ok: r.status === 0, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
49
+ };
50
+
51
+ export interface BoundsRatchetVerdict {
52
+ ok: boolean;
53
+ violations: string[];
54
+ /** The bound read at the base ref, or `null` when it could not be read. */
55
+ baseGraceCeiling: number | null;
56
+ /** The ref compared against, or `null` when none resolved. */
57
+ baseRef: string | null;
58
+ /** Why the comparison was skipped, when it was. Printed, never silent. */
59
+ note: string | null;
60
+ }
61
+
62
+ function showAtRef(git: GitRunner, repoRoot: string, ref: string, relPath: string): string | null {
63
+ const r = git(['show', `${ref}:${relPath}`], repoRoot);
64
+ return r.ok ? r.stdout : null;
65
+ }
66
+
67
+ /**
68
+ * Compare the grace ceiling in this tree against the one at the base ref.
69
+ *
70
+ * SKIPS rather than fails when the base ref or the base config cannot be read.
71
+ * That is the opposite of the fail-closed posture the same council demanded for
72
+ * the reserve, and the asymmetry is deliberate: an unreadable base there would
73
+ * have GRANTED budget on an infrastructure failure, whereas here it would REFUSE
74
+ * a change on one. A shallow clone, a first commit and a detached build all
75
+ * legitimately have no base, and a gate that reds on them gets switched off —
76
+ * which is how a repository loses the enforcement it just built. The skip is
77
+ * reported in both the human and JSON output so it cannot pass for a check.
78
+ */
79
+ export function assertBoundsDidNotRise(opts: {
80
+ repoRoot: string;
81
+ baseRef: string | null;
82
+ git?: GitRunner;
83
+ /**
84
+ * The ceiling this run actually compares the payload against.
85
+ *
86
+ * The CI step passes `--ceiling <grace_ceiling>`, so the effective bound can
87
+ * be raised either by editing the config or by passing a bigger number. Both
88
+ * arrive here as one value, which closes the second path without a second
89
+ * check.
90
+ */
91
+ headGraceCeiling: number;
92
+ }): BoundsRatchetVerdict {
93
+ const git = opts.git ?? realGit;
94
+ if (opts.baseRef === null || opts.baseRef.trim() === '') {
95
+ return {
96
+ ok: true,
97
+ violations: [],
98
+ baseGraceCeiling: null,
99
+ baseRef: null,
100
+ note: 'no base ref resolved, so the shrink-only bound was NOT verified in this run',
101
+ };
102
+ }
103
+ const cfg = showAtRef(git, opts.repoRoot, opts.baseRef, BUDGET_CONFIG_PATH);
104
+ if (cfg === null) {
105
+ return {
106
+ ok: true,
107
+ violations: [],
108
+ baseGraceCeiling: null,
109
+ baseRef: opts.baseRef,
110
+ note: `${BUDGET_CONFIG_PATH} could not be read at ${opts.baseRef}, so the shrink-only bound was NOT verified`,
111
+ };
112
+ }
113
+ let baseGrace: number | null = null;
114
+ try {
115
+ const parsed = JSON.parse(cfg) as Record<string, unknown>;
116
+ const delivery = (parsed['ci_delivery'] ?? {}) as Record<string, unknown>;
117
+ const n = Number(delivery['grace_ceiling']);
118
+ baseGrace = Number.isFinite(n) ? n : null;
119
+ } catch {
120
+ return {
121
+ ok: true,
122
+ violations: [],
123
+ baseGraceCeiling: null,
124
+ baseRef: opts.baseRef,
125
+ note: `${BUDGET_CONFIG_PATH} at ${opts.baseRef} is not parseable, so the shrink-only bound was NOT verified`,
126
+ };
127
+ }
128
+ if (baseGrace === null) {
129
+ return {
130
+ ok: true,
131
+ violations: [],
132
+ baseGraceCeiling: null,
133
+ baseRef: opts.baseRef,
134
+ note: `no ci_delivery.grace_ceiling at ${opts.baseRef}, so there is no earlier bound to ratchet against`,
135
+ };
136
+ }
137
+ if (opts.headGraceCeiling > baseGrace) {
138
+ return {
139
+ ok: false,
140
+ baseGraceCeiling: baseGrace,
141
+ baseRef: opts.baseRef,
142
+ note: null,
143
+ violations: [
144
+ `the standing-payload grace ceiling rose from ${String(baseGrace)} to ` +
145
+ `${String(opts.headGraceCeiling)} against ${opts.baseRef}. ` +
146
+ `${BUDGET_CONFIG_PATH} says it may not move upward and ADR-264 decided that ` +
147
+ 'the sentence stands: standing-rule growth needs a compensating reduction ' +
148
+ 'elsewhere, not a bigger ceiling. Lower the addition, migrate the prose out of ' +
149
+ 'the standing rule, or reduce another standing asset by the same amount.',
150
+ ],
151
+ };
152
+ }
153
+ return { ok: true, violations: [], baseGraceCeiling: baseGrace, baseRef: opts.baseRef, note: null };
154
+ }
@@ -143,10 +143,68 @@ export const CAPSULE_SCHEMA_VERSION = 4;
143
143
  */
144
144
  export const ACCEPTED_CAPSULE_VERSIONS: readonly number[] = [3, 4];
145
145
 
146
- /** The two CHECKPOINT variants sharing this schema. */
147
- export const CAPSULE_VARIANTS = ['worker', 'main_session'] as const;
146
+ /** The three CHECKPOINT variants sharing this schema. */
147
+ export const CAPSULE_VARIANTS = ['worker', 'main_session', 'continuity_record'] as const;
148
148
  export type CapsuleVariant = (typeof CAPSULE_VARIANTS)[number];
149
149
 
150
+ /**
151
+ * `continuity_record` — the DETERMINISTIC main-session variant.
152
+ *
153
+ * Why a variant and not a relaxed `main_session`. `main_session` requires
154
+ * `failed_approaches` and `successful_approaches` with at least one entry each,
155
+ * and requires the explicit token `none` when nothing failed or worked — the
156
+ * whole point of those fields being that silence and "nothing was abandoned"
157
+ * stay distinguishable. A writer that runs without model spend cannot know
158
+ * either. It could only write `none`, which asserts a judgement nobody made.
159
+ *
160
+ * The AI council of 2026-09-07 ruled exactly this: add a variant rather than
161
+ * stuffing absence markers into `main_session`, because explicit absence
162
+ * markers "misrepresent unavailable model judgments as values". This constant
163
+ * set is that ruling in code.
164
+ *
165
+ * The fields below are FORBIDDEN on this variant rather than optional, and the
166
+ * distinction is load-bearing. Optional would leave the door open for a later
167
+ * writer to fill one with a guess and for a reader to be unable to tell a
168
+ * derived record from a judged one. Forbidden makes the variant's meaning
169
+ * checkable: everything a `continuity_record` carries was computed from disk.
170
+ *
171
+ * Nothing here is invented — every name is an existing key of this same schema.
172
+ * The council's constraint was explicit: the required fields "must be derived
173
+ * from the existing validated envelope contract; this ruling does not authorize
174
+ * inventing parallel identity or directive fields."
175
+ *
176
+ * Version compatibility is unchanged and envelope-wide: a `continuity_record`
177
+ * declares the same `capsule_version`, and a reader that does not know the
178
+ * variant refuses it by name (see `validateRecycleEnvelope`) rather than
179
+ * guessing at its required set.
180
+ */
181
+ export const CONTINUITY_RECORD_FORBIDDEN_KEYS: ReadonlySet<string> = new Set([
182
+ 'decisions',
183
+ 'constraints',
184
+ 'do_not_touch',
185
+ 'assumptions',
186
+ 'failed_approaches',
187
+ 'successful_approaches',
188
+ 'open_questions',
189
+ 'not_carried_forward',
190
+ 'status_summary',
191
+ 'suggested_skills',
192
+ 'open_worker_envelopes',
193
+ ]);
194
+
195
+ /** Required on a `continuity_record` — each one computable from on-disk state. */
196
+ export const CONTINUITY_RECORD_REQUIRED_KEYS: readonly string[] = [
197
+ 'capsule_version',
198
+ 'variant',
199
+ 'summary',
200
+ 'task',
201
+ 'workspace',
202
+ 'written_at',
203
+ 'acceptance_criteria',
204
+ 'remaining',
205
+ 'predecessor',
206
+ ];
207
+
150
208
  /** A single-line token no longer than `max` chars. Rejects any body-shaped value. */
151
209
  function isShortLine(value: unknown, max: number): value is string {
152
210
  return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\n');
@@ -757,6 +815,31 @@ export function validateRecycleEnvelope(input: unknown): string[] {
757
815
  }
758
816
  }
759
817
 
818
+ // VARIANT FIRST, then version. The order is the contract, not a style
819
+ // choice. Required fields are variant-specific, so a reader that does not
820
+ // recognise the variant cannot know what to demand — and a reader that
821
+ // reports twenty field violations against a variant it never understood has
822
+ // told the operator nothing useful and, worse, has implied the record was
823
+ // malformed when the truth is that the reader is old. One refusal naming
824
+ // the variant is the honest output, and it is what makes an old reader
825
+ // meeting a new record a legible event rather than a pile of noise.
826
+ const variant = e['variant'];
827
+ if (typeof variant !== 'string' || !(CAPSULE_VARIANTS as readonly string[]).includes(variant)) {
828
+ return [
829
+ `unknown variant ${JSON.stringify(variant)} — this reader knows ` +
830
+ `${CAPSULE_VARIANTS.join(' | ')}. A reader that does not know a variant must not ` +
831
+ 'guess at its required fields, so no further field checks were run.',
832
+ ];
833
+ }
834
+ if (variant === 'worker') {
835
+ return [
836
+ "variant 'worker' is a subagent generation handover and is validated by the worker " +
837
+ 'path, not by the main-session recycle validator — routing it here would apply ' +
838
+ 'the wrong required set to a well-formed record.',
839
+ ];
840
+ }
841
+ const isDerived = variant === 'continuity_record';
842
+
760
843
  const version = e['capsule_version'];
761
844
  if (typeof version !== 'number' || !ACCEPTED_CAPSULE_VERSIONS.includes(version)) {
762
845
  errors.push(`capsule_version must be one of ${ACCEPTED_CAPSULE_VERSIONS.join(' | ')}`);
@@ -766,9 +849,35 @@ export function validateRecycleEnvelope(input: unknown): string[] {
766
849
  // Demanding the new fields of an already-written record would be a
767
850
  // retroactive requirement, which is the one thing the recorded schema lock
768
851
  // forbids ("add fields or variants, never repurpose or remove one").
769
- const atV4 = version === 4;
770
- if (e['variant'] !== 'main_session') {
771
- errors.push("variant must be 'main_session'");
852
+ //
853
+ // A `continuity_record` is exempt from the two v4 judgement fields for the
854
+ // opposite reason: it is forbidden from carrying them at all (below), so
855
+ // requiring them would make the variant unsatisfiable.
856
+ const atV4 = version === 4 && !isDerived;
857
+
858
+ if (isDerived) {
859
+ // Forbidden, not merely absent: a derived record that carries a
860
+ // judgement field is either a `main_session` mislabelled, or a
861
+ // deterministic writer that invented a value. Both are worth refusing
862
+ // loudly, and neither is distinguishable after the fact.
863
+ for (const key of Object.keys(e)) {
864
+ if (CONTINUITY_RECORD_FORBIDDEN_KEYS.has(key)) {
865
+ errors.push(
866
+ `field "${key}" is forbidden on variant 'continuity_record' — it carries a ` +
867
+ 'model judgement, and a record written without model spend cannot have made ' +
868
+ "one. Use variant 'main_session' for a judged record.",
869
+ );
870
+ }
871
+ }
872
+ for (const key of CONTINUITY_RECORD_REQUIRED_KEYS) {
873
+ if (e[key] === undefined) {
874
+ errors.push(
875
+ `field "${key}" is required on variant 'continuity_record' — it is computable ` +
876
+ 'from on-disk state, so its absence is a writer defect rather than an ' +
877
+ 'unavailable judgement.',
878
+ );
879
+ }
880
+ }
772
881
  }
773
882
  if (!isShortLine(e['summary'], MAX_LINE_CHARS)) {
774
883
  errors.push(`summary must be a single line of 1–${MAX_LINE_CHARS} chars`);
@@ -789,7 +898,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
789
898
  errors.push('acceptance_criteria must carry at least one entry — the successor cannot know "done" without it');
790
899
  }
791
900
  checkList(errors, 'remaining', e['remaining'], MAX_LINE_CHARS, true);
792
- checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, true);
901
+ checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, !isDerived);
793
902
  checkList(errors, 'decisions', e['decisions'], MAX_LINE_CHARS, false);
794
903
  errors.push(...decisionTagErrors(e['decisions']));
795
904
  checkList(errors, 'constraints', e['constraints'], MAX_LINE_CHARS, false);
@@ -809,7 +918,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
809
918
 
810
919
  // Required with ≥ 1 entry (Phase 2.3): a session that abandoned nothing
811
920
  // writes `none`. Absence must never be readable as "nothing failed".
812
- checkList(errors, 'failed_approaches', e['failed_approaches'], MAX_LINE_CHARS, true);
921
+ checkList(errors, 'failed_approaches', e['failed_approaches'], MAX_LINE_CHARS, !isDerived);
813
922
  if (Array.isArray(e['failed_approaches']) && e['failed_approaches'].length === 0) {
814
923
  errors.push(
815
924
  'failed_approaches must carry at least one entry — write "none" explicitly, so ' +
@@ -835,7 +944,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
835
944
  // is a reader waiting for something that will not arrive.
836
945
  const predecessor = e['predecessor'];
837
946
  if (predecessor === undefined) {
838
- if (atV4) {
947
+ if (atV4 || isDerived) {
839
948
  errors.push(
840
949
  'predecessor is required — name the session this one continues, or write "none" ' +
841
950
  'explicitly for the first session in a workspace',
@@ -0,0 +1,280 @@
1
+ /**
2
+ * The vendored grammars' one anchor outside the manifest.
3
+ *
4
+ * WHAT THIS ANSWERS.
5
+ *
6
+ * `packed_binary_predicate.ts` verifies eleven conditions over
7
+ * `src/config/packed-binary-manifest.json`, and every one of them is
8
+ * manifest-internal: the manifest states a sha256 and the bytes are hashed
9
+ * against it, so a single commit that edits both passes all eleven. The
10
+ * `release/14.22.0` self-review finding `d1696732ac28` names exactly that gap.
11
+ * This module closes it by comparing the vendored bytes against a source the
12
+ * repository does not author: the `tree-sitter-wasms` release pinned in
13
+ * `package-lock.json` and installed by `npm ci`.
14
+ *
15
+ * WHAT IT MAY CLAIM, AND WHAT IT MAY NOT.
16
+ *
17
+ * It MAY claim: the vendored bytes equal the bytes of the locked upstream
18
+ * package, independent of the binary manifest's own self-description.
19
+ *
20
+ * It MUST NOT claim to be independent of all repository state, and it MUST NOT
21
+ * claim to verify the grammars cryptographically against the registry
22
+ * independent of local build state. It does neither. The chain is
23
+ * registry tarball -> `npm ci` checks its sha512 against the lock's
24
+ * `integrity` -> extracted bytes -> vendored bytes, and only the last link is
25
+ * checked here. An attacker who edits `node_modules` after install defeats
26
+ * this, and a lock entry that points at a hostile package defeats it earlier
27
+ * still — that link is review's job, not a check's, because the lock is
28
+ * human-readable text and the manifest is machine-only JSON.
29
+ *
30
+ * Both scoping sentences are the AI council's wording (2026-09-09, 2 seats,
31
+ * unanimous on this question), kept verbatim so a later reader can tell the
32
+ * boundary from an oversight.
33
+ *
34
+ * WHY IT REFUSES RATHER THAN SKIPS.
35
+ *
36
+ * An upstream comparison is trivially written so that it passes when the
37
+ * upstream is absent, which is a green that verifies nothing — the exact shape
38
+ * of the eleven conditions it is here to supplement. So an absent package, a
39
+ * version that is not the locked one, an absent counterpart file, an empty
40
+ * claim set, and two admitted entries sharing a filename are all refusals,
41
+ * never passes.
42
+ *
43
+ * The last of those is the subtlest, and it was added on an R2 review finding
44
+ * rather than foreseen: the upstream keeps its grammars in one flat directory,
45
+ * so the counterpart is addressed by basename. Two manifest rows with the same
46
+ * filename in different directories would anchor to the same upstream file —
47
+ * one admitted binary silently unanchored while the verdict reported full
48
+ * coverage, which is the false green this module exists to remove. The vendored
49
+ * side resolves the entry's OWN repo-relative path, never a basename under a
50
+ * fixed directory, for the same reason.
51
+ */
52
+ import { createHash } from 'node:crypto';
53
+ import * as fs from 'node:fs';
54
+ import * as path from 'node:path';
55
+
56
+ import { loadManifest, VERIFIABLE_KIND, type PackedBinaryManifest } from './packed_binary_predicate.js';
57
+
58
+ /**
59
+ * The upstream this repository copies from. Hardcoded rather than derived: the
60
+ * vendored set exists precisely because the package is NOT a runtime
61
+ * dependency, so there is no dependency edge to read it off, and
62
+ * `src/vendor/grammars/README.md` names the same string as the refresh source.
63
+ */
64
+ export const UPSTREAM_PACKAGE = 'tree-sitter-wasms';
65
+
66
+ /** Where the upstream keeps its compiled grammars inside its own package. */
67
+ const UPSTREAM_GRAMMAR_SUBDIR = 'out';
68
+
69
+ /** A refusal — the anchor could not be established, so no verdict is honest. */
70
+ export class UpstreamAnchorRefusal extends Error {
71
+ constructor(message: string) {
72
+ super(message);
73
+ this.name = 'UpstreamAnchorRefusal';
74
+ }
75
+ }
76
+
77
+ /** One vendored file compared against its upstream counterpart. */
78
+ export interface GrammarComparison {
79
+ readonly vendoredPath: string;
80
+ readonly upstreamPath: string;
81
+ readonly vendoredSha256: string;
82
+ readonly upstreamSha256: string;
83
+ readonly equal: boolean;
84
+ }
85
+
86
+ export interface UpstreamAnchorVerdict {
87
+ readonly lockedVersion: string;
88
+ readonly lockedIntegrity: string;
89
+ readonly installedVersion: string;
90
+ readonly comparisons: readonly GrammarComparison[];
91
+ /** Human-readable divergence lines. Empty iff every comparison is equal. */
92
+ readonly divergences: readonly string[];
93
+ }
94
+
95
+ export interface UpstreamAnchorOptions {
96
+ readonly repoRoot: string;
97
+ /** Defaults to `<repoRoot>/node_modules/<UPSTREAM_PACKAGE>`. Fixture seam. */
98
+ readonly upstreamPackageDir?: string;
99
+ /**
100
+ * Root the manifest's repo-relative `path` values resolve against.
101
+ * Defaults to `repoRoot`. Fixture seam — NOT a grammar directory: resolving
102
+ * by directory would discard the entry's own path, and two rows sharing a
103
+ * basename in different directories would then anchor to the same file.
104
+ */
105
+ readonly vendoredRoot?: string;
106
+ /** Defaults to the manifest at `<repoRoot>/src/config/…`. Fixture seam. */
107
+ readonly manifest?: PackedBinaryManifest;
108
+ }
109
+
110
+ export interface LockPin {
111
+ readonly version: string;
112
+ readonly integrity: string;
113
+ }
114
+
115
+ /**
116
+ * The locked pin for the upstream, read from `package-lock.json`.
117
+ *
118
+ * `integrity` is not used to verify anything here — it is carried into the
119
+ * verdict so the caller can print the anchor it is trusting, and its absence
120
+ * is a refusal because a lock row without one pins a version and not bytes.
121
+ */
122
+ export function readLockPin(repoRoot: string): LockPin {
123
+ const lockPath = path.join(repoRoot, 'package-lock.json');
124
+ if (!fs.existsSync(lockPath)) {
125
+ throw new UpstreamAnchorRefusal(`no package-lock.json at ${lockPath} — there is no locked upstream to anchor to`);
126
+ }
127
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8')) as {
128
+ packages?: Record<string, { version?: string; integrity?: string }>;
129
+ };
130
+ const key = `node_modules/${UPSTREAM_PACKAGE}`;
131
+ const row = lock.packages?.[key];
132
+ if (row === undefined) {
133
+ throw new UpstreamAnchorRefusal(`package-lock.json carries no \`${key}\` row — the vendored grammars have no locked source`);
134
+ }
135
+ if (typeof row.version !== 'string' || row.version.length === 0) {
136
+ throw new UpstreamAnchorRefusal(`\`${key}\` in package-lock.json carries no version`);
137
+ }
138
+ if (typeof row.integrity !== 'string' || row.integrity.length === 0) {
139
+ throw new UpstreamAnchorRefusal(
140
+ `\`${key}\` in package-lock.json carries no \`integrity\` — the row pins a version but not bytes, so \`npm ci\` verifies nothing for it`,
141
+ );
142
+ }
143
+ return { version: row.version, integrity: row.integrity };
144
+ }
145
+
146
+ /**
147
+ * The manifest, read through the predicate's own reader.
148
+ *
149
+ * A second reader would drift: a shape condition added there — a
150
+ * `schema_version` check, a kind allowlist — would leave this module accepting
151
+ * a manifest the predicate rejects, and this module's whole value is that it
152
+ * covers what the manifest admits. So `loadManifest` is the reader and this
153
+ * wrapper only re-types its failures as refusals, because an unreadable
154
+ * manifest means the anchor was not established rather than that it failed.
155
+ * R2 finding 2, 2026-09-09 — the duplicate reader this replaces was one
156
+ * identifier away from the import already at the top of this file.
157
+ */
158
+ function readManifest(repoRoot: string): PackedBinaryManifest {
159
+ try {
160
+ return loadManifest(repoRoot);
161
+ } catch (err) {
162
+ throw new UpstreamAnchorRefusal(
163
+ `the packed-binary manifest could not be read, so nothing was compared: ${err instanceof Error ? err.message : String(err)}`,
164
+ );
165
+ }
166
+ }
167
+
168
+ function sha256(file: string): string {
169
+ return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
170
+ }
171
+
172
+ /**
173
+ * Compare every vendored grammar the manifest admits against the locked
174
+ * upstream's copy of the same filename.
175
+ *
176
+ * Throws `UpstreamAnchorRefusal` when the anchor cannot be established at all;
177
+ * returns a verdict whose `divergences` is non-empty when it can be established
178
+ * and the bytes differ. The two are deliberately different outcomes: a refusal
179
+ * says "this was not checked", a divergence says "this was checked and is
180
+ * wrong", and collapsing them is how a check becomes a permanent green.
181
+ */
182
+ export function verifyVendoredGrammarsAgainstUpstream(options: UpstreamAnchorOptions): UpstreamAnchorVerdict {
183
+ const { repoRoot } = options;
184
+ const upstreamDir = options.upstreamPackageDir ?? path.join(repoRoot, 'node_modules', UPSTREAM_PACKAGE);
185
+ const vendoredRoot = options.vendoredRoot ?? repoRoot;
186
+ const manifest = options.manifest ?? readManifest(repoRoot);
187
+
188
+ const pin = readLockPin(repoRoot);
189
+
190
+ const claims = manifest.entries.filter((e) => e.kind === VERIFIABLE_KIND);
191
+ if (claims.length === 0) {
192
+ throw new UpstreamAnchorRefusal(
193
+ `the manifest admits no \`${VERIFIABLE_KIND}\` entry — nothing was compared, and reporting that as a pass would be a green over an empty corpus`,
194
+ );
195
+ }
196
+
197
+ // The upstream keeps its grammars in one flat directory, so the counterpart
198
+ // is addressed by basename. Two admitted entries sharing a basename would
199
+ // therefore point at the same upstream file, which is an ambiguous anchor
200
+ // rather than a comparison — refuse instead of picking one.
201
+ const byBase = new Map<string, string[]>();
202
+ for (const e of claims) {
203
+ const base = path.basename(e.path);
204
+ byBase.set(base, [...(byBase.get(base) ?? []), e.path]);
205
+ }
206
+ const ambiguous = [...byBase.entries()].filter(([, paths]) => paths.length > 1);
207
+ if (ambiguous.length > 0) {
208
+ throw new UpstreamAnchorRefusal(
209
+ `two or more admitted entries share a filename, so they would anchor to the same upstream file: ${ambiguous
210
+ .map(([base, paths]) => `${base} <- ${paths.join(', ')}`)
211
+ .join('; ')}`,
212
+ );
213
+ }
214
+
215
+ const upstreamPkgJson = path.join(upstreamDir, 'package.json');
216
+ if (!fs.existsSync(upstreamPkgJson)) {
217
+ throw new UpstreamAnchorRefusal(
218
+ `${UPSTREAM_PACKAGE} is not installed at ${upstreamDir} — run \`npm ci\`. The vendored bytes were NOT verified; an absent upstream is a refusal, never a pass`,
219
+ );
220
+ }
221
+ const installed = JSON.parse(fs.readFileSync(upstreamPkgJson, 'utf-8')) as { version?: string };
222
+ if (installed.version !== pin.version) {
223
+ throw new UpstreamAnchorRefusal(
224
+ `installed ${UPSTREAM_PACKAGE} is ${String(installed.version)} but package-lock.json pins ${pin.version} — comparing against an unlocked tree would anchor to nothing`,
225
+ );
226
+ }
227
+
228
+ const comparisons: GrammarComparison[] = [];
229
+ const divergences: string[] = [];
230
+
231
+ for (const entry of claims) {
232
+ const vendoredPath = path.join(vendoredRoot, entry.path);
233
+ const upstreamPath = path.join(upstreamDir, UPSTREAM_GRAMMAR_SUBDIR, path.basename(entry.path));
234
+ if (!fs.existsSync(vendoredPath)) {
235
+ throw new UpstreamAnchorRefusal(`the manifest admits ${entry.path} but ${vendoredPath} does not exist`);
236
+ }
237
+ if (!fs.existsSync(upstreamPath)) {
238
+ throw new UpstreamAnchorRefusal(
239
+ `${entry.path} has no counterpart at ${upstreamPath} — the locked upstream does not carry this grammar, so this file has no anchor`,
240
+ );
241
+ }
242
+ const vendoredSha256 = sha256(vendoredPath);
243
+ const upstreamSha256 = sha256(upstreamPath);
244
+ const equal = vendoredSha256 === upstreamSha256;
245
+ comparisons.push({ vendoredPath, upstreamPath, vendoredSha256, upstreamSha256, equal });
246
+ if (!equal) {
247
+ divergences.push(
248
+ `${entry.path}: vendored sha256 ${vendoredSha256} != upstream ${upstreamSha256} (${UPSTREAM_PACKAGE}@${pin.version})`,
249
+ );
250
+ }
251
+ }
252
+
253
+ return buildVerdict({ pin, installedVersion: installed.version, comparisons, divergences });
254
+ }
255
+
256
+ /**
257
+ * Assemble the verdict from the two version sources, kept separate.
258
+ *
259
+ * Extracted so the sourcing is testable at all. On any tree the check accepts,
260
+ * `pin.version` and the installed version are equal — the strict guard above
261
+ * refuses otherwise — so a test driving the whole function can never tell which
262
+ * of the two the `installedVersion` field came from, and the round-1 regression
263
+ * test for exactly that defect passed with the defect restored. R2 finding 1,
264
+ * 2026-09-09. Here the two can be given different values, which is the only
265
+ * place the distinction is observable.
266
+ */
267
+ export function buildVerdict(args: {
268
+ readonly pin: LockPin;
269
+ readonly installedVersion: string;
270
+ readonly comparisons: readonly GrammarComparison[];
271
+ readonly divergences: readonly string[];
272
+ }): UpstreamAnchorVerdict {
273
+ return {
274
+ lockedVersion: args.pin.version,
275
+ lockedIntegrity: args.pin.integrity,
276
+ installedVersion: args.installedVersion,
277
+ comparisons: args.comparisons,
278
+ divergences: args.divergences,
279
+ };
280
+ }
@@ -47,6 +47,11 @@ export function curlJsonPost(url: string, extraHeaders: string[], body: unknown)
47
47
  // before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
48
48
  // A full 16k-token generation legitimately runs several minutes, so the
49
49
  // ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
50
+ // Both ceilings are PER CALL, never a run total. Members are iterated
51
+ // serially, so a run's worst case is members x passes x 290s, and nothing
52
+ // cancels a call mid-flight: `spawnSync` takes no signal, which is the
53
+ // synchronous contract the module docblock states. The only run-level bound
54
+ // is the USD cap, and it is checked AFTER a call returns.
50
55
  args.push('--connect-timeout', '30', '--max-time', '290');
51
56
  args.push('-w', '\n%{http_code}', '--data-binary', '@-');
52
57
  const r = spawnSync('curl', args, {