@event4u/agent-config 16.0.0 → 16.1.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 (246) 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 +297 -137
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +74 -27
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  24. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
  25. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  26. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
  27. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  28. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  29. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  30. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  31. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  32. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  33. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  34. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  35. package/dist/agent-src/rules/delegation-policy.md +23 -45
  36. package/dist/agent-src/rules/design-fidelity.md +32 -37
  37. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  38. package/dist/agent-src/rules/test-first.md +41 -0
  39. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  40. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  41. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  42. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  43. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  44. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  45. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  46. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  47. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  48. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  49. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  50. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  52. package/dist/agent-src/templates/agent-settings.md +1 -1
  53. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  54. package/dist/agent-src/templates/roadmaps.md +188 -0
  55. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  56. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  58. package/dist/cli-delegate/check_update_banner.js +40 -2
  59. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  60. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  61. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  62. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  63. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  64. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  65. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  66. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  67. package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
  68. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  69. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  70. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  71. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  72. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  73. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  74. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  75. package/dist/cli-delegate/cmd_conformance.js +90 -56
  76. package/dist/cli-delegate/cmd_doctor.js +7 -5
  77. package/dist/cli-delegate/cmd_explain.js +2 -2
  78. package/dist/cli-delegate/cmd_export.js +9 -7
  79. package/dist/cli-delegate/cmd_fleet.js +4 -2
  80. package/dist/cli-delegate/cmd_handoff.js +4 -4
  81. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  82. package/dist/cli-delegate/cmd_migrate.js +2 -2
  83. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  84. package/dist/cli-delegate/cmd_preflight.js +4 -2
  85. package/dist/cli-delegate/cmd_prune.js +10 -8
  86. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  87. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  88. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  89. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  90. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  91. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  92. package/dist/cli-delegate/cmd_sync.js +10 -8
  93. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  94. package/dist/cli-delegate/cmd_update.js +1 -1
  95. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  96. package/dist/cli-delegate/cmd_validate.js +12 -10
  97. package/dist/cli-delegate/cmd_versions.js +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +85 -38
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +4 -4
  102. package/dist/discovery/dormancy-report.md +1 -1
  103. package/dist/discovery/orphan-report.md +1 -1
  104. package/dist/discovery/packs.json +10 -8
  105. package/dist/discovery/trust-report.md +3 -3
  106. package/dist/discovery/workspaces.json +8 -6
  107. package/dist/hooks/dispatch.js +1832 -1007
  108. package/dist/install/claudeRuleRewrite.js +14 -3
  109. package/dist/install/conflict.js +60 -10
  110. package/dist/install/conflictTracker.js +218 -0
  111. package/dist/install/fsPrimitives.js +54 -0
  112. package/dist/install/install.mjs +2007 -1481
  113. package/dist/install/manifestFiles.js +259 -0
  114. package/dist/install/preflight.js +14 -2
  115. package/dist/install/preserve.js +157 -0
  116. package/dist/install/recordedOwnership.js +132 -0
  117. package/dist/install/txlog.js +11 -2
  118. package/dist/mcp/registry-manifest.json +3 -3
  119. package/dist/mcp/server.json +2 -2
  120. package/dist/mcp/server.mjs +398 -39
  121. package/dist/router.json +1 -1
  122. package/dist/scripts/_lib/agent_settings.js +3 -2
  123. package/dist/scripts/_lib/fs_atomic.js +167 -0
  124. package/dist/scripts/_lib/installed_tools.js +460 -0
  125. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  126. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  127. package/dist/server/routes/install.js +8 -0
  128. package/dist/server/routes/wizard.js +9 -3
  129. package/dist/server/schemas/settings.js +17 -5
  130. package/dist/shared/missionExecution.js +97 -0
  131. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  132. package/dist/ui/index.html +1 -1
  133. package/docs/MIGRATION.md +103 -0
  134. package/docs/contracts/settings-classes.md +26 -7
  135. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  136. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  137. package/docs/guidelines/design-fidelity-routing.md +29 -0
  138. package/package.json +1 -1
  139. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  140. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  141. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  142. package/src/config/agent-settings.template.yml +139 -27
  143. package/src/config/auto-rule-budget.json +16 -0
  144. package/src/config/ci-local-parity.yml +18 -5
  145. package/src/config/continuity-surface.json +21 -0
  146. package/src/config/gate-coverage.yml +146 -13
  147. package/src/config/gate-violation-baselines.json +6 -6
  148. package/src/config/hook-token-budget.json +2 -0
  149. package/src/config/loop-surfaces.yaml +215 -0
  150. package/src/config/platform-anchor.json +3 -2
  151. package/src/config/release-gate-locality.yml +11 -0
  152. package/src/config/routing-coverage-seed.json +18 -4
  153. package/src/config/rule-enforcement-baseline.json +0 -1
  154. package/src/install/claudeRuleRewrite.ts +16 -2
  155. package/src/install/conflict.ts +65 -9
  156. package/src/install/conflictTracker.ts +299 -0
  157. package/src/install/fsPrimitives.ts +54 -0
  158. package/src/install/manifestFiles.ts +312 -0
  159. package/src/install/preflight.ts +16 -2
  160. package/src/install/preserve.ts +205 -0
  161. package/src/install/recordedOwnership.ts +149 -0
  162. package/src/install/txlog.ts +11 -2
  163. package/src/install/types.ts +19 -4
  164. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  165. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  166. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  167. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  168. package/src/scripts/_cli/doctor_execution.ts +90 -0
  169. package/src/scripts/_lib/activation_payload.ts +247 -0
  170. package/src/scripts/_lib/agent_settings.ts +4 -2
  171. package/src/scripts/_lib/authority_path.ts +102 -0
  172. package/src/scripts/_lib/cascade_base.ts +94 -0
  173. package/src/scripts/_lib/changelog_eras.ts +7 -2
  174. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  175. package/src/scripts/_lib/council_transport.ts +129 -0
  176. package/src/scripts/_lib/delivery_ready.ts +112 -0
  177. package/src/scripts/_lib/forge_protection.ts +267 -0
  178. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  179. package/src/scripts/_lib/json_python_parity.ts +127 -0
  180. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  181. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  182. package/src/scripts/_lib/mission_record.ts +190 -0
  183. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  184. package/src/scripts/_lib/obligations.ts +402 -0
  185. package/src/scripts/_lib/orchestration_record.ts +11 -20
  186. package/src/scripts/_lib/release_highlights.ts +199 -10
  187. package/src/scripts/_lib/release_holds.ts +493 -0
  188. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  189. package/src/scripts/_lib/rides_along.ts +102 -0
  190. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  191. package/src/scripts/_lib/test_delta.ts +166 -0
  192. package/src/scripts/_lib/test_provenance.ts +171 -0
  193. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  194. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  195. package/src/scripts/ai_council/config.ts +8 -11
  196. package/src/scripts/ai_council/fallback_config.ts +5 -3
  197. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  198. package/src/scripts/ask_block_census.ts +285 -15
  199. package/src/scripts/check_enforcement_matrix.ts +624 -0
  200. package/src/scripts/check_gate_reachability.ts +173 -10
  201. package/src/scripts/check_generator_sync.ts +954 -0
  202. package/src/scripts/check_preamble_payload_budget.ts +75 -2
  203. package/src/scripts/check_release_highlights.ts +33 -1
  204. package/src/scripts/check_release_holds.ts +567 -0
  205. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  206. package/src/scripts/check_test_delta.ts +294 -0
  207. package/src/scripts/check_test_weakening.ts +299 -0
  208. package/src/scripts/closure_scan.ts +333 -0
  209. package/src/scripts/compile_hook_manifest.ts +43 -3
  210. package/src/scripts/council_record_shape.ts +209 -0
  211. package/src/scripts/hook_manifest.json +1 -1
  212. package/src/scripts/hook_manifest.yaml +88 -9
  213. package/src/scripts/hooks/concern_registry.ts +4 -0
  214. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  215. package/src/scripts/hooks/evidence_independence.ts +96 -0
  216. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  217. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  218. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  219. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  220. package/src/scripts/hooks_doctor.ts +85 -0
  221. package/src/scripts/hooks_status.ts +19 -0
  222. package/src/scripts/install.ts +106 -206
  223. package/src/scripts/janitor.ts +7 -0
  224. package/src/scripts/lint_decision_classes.ts +464 -0
  225. package/src/scripts/lint_hook_manifest.ts +11 -0
  226. package/src/scripts/lint_major_migration_sections.ts +450 -0
  227. package/src/scripts/lint_plan_risk_register.ts +47 -3
  228. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  229. package/src/scripts/lint_roadmap_producers.ts +202 -0
  230. package/src/scripts/measure_rule_budget.ts +101 -5
  231. package/src/scripts/new_roadmap.ts +37 -0
  232. package/src/scripts/orchestration_record.ts +0 -5
  233. package/src/scripts/release.ts +14 -98
  234. package/src/scripts/release_drill.ts +267 -1
  235. package/src/scripts/release_publication.ts +51 -0
  236. package/src/scripts/report_enforcement_drift.ts +566 -0
  237. package/src/scripts/report_host_injection_effect.ts +3 -11
  238. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  239. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  240. package/src/scripts/schemas/command.schema.json +4 -0
  241. package/src/scripts/scope_growth.ts +219 -0
  242. package/src/scripts/trigger_eval_grandfather.json +0 -1
  243. package/src/scripts/ui_conformance_probe.ts +622 -0
  244. package/src/server/schemas/settings.ts +31 -7
  245. package/src/shared/missionExecution.ts +126 -0
  246. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -11,11 +11,22 @@
11
11
  *
12
12
  * So each side supplies the raw string from its own reader and this module
13
13
  * decides what it means. Anything unrecognised — absent key, typo, `null`,
14
- * a non-string — is `eager-all`, today's shipped behaviour: a mode nobody can
15
- * spell must never silently thin the standing corpus.
14
+ * a non-string — is `eager-all`: the parser fallback, which is NOT the value
15
+ * the template ships (see the type below), because a mode nobody can spell
16
+ * must never silently thin the standing corpus.
16
17
  */
17
18
 
18
- /** The three projection shapes. `eager-all` is the shipped default. */
19
+ /**
20
+ * The three projection shapes, and the two different defaults they answer to.
21
+ *
22
+ * The TEMPLATE default — what a consumer is given — is `delivery` with
23
+ * `hosts: [claude-code]`, shipped in `src/config/agent-settings.template.yml`
24
+ * since ADR-267, so on that one host a normal read resolves to `delivery`.
25
+ * The PARSER FALLBACK — what applies when no value resolves from any layer —
26
+ * is `eager-all` (`DEFAULT_LEAN_PROJECTION_MODE` below), deliberately not
27
+ * flipped with the template per ADR-267 decision 4: the template says what was
28
+ * chosen, the constant says what happens when nothing could be read.
29
+ */
19
30
  export type LeanProjectionMode = 'eager-all' | 'thin' | 'delivery';
20
31
 
21
32
  export const DEFAULT_LEAN_PROJECTION_MODE: LeanProjectionMode = 'eager-all';
@@ -0,0 +1,382 @@
1
+ /**
2
+ * The loop-surface inventory, and the reach question it exists to answer.
3
+ *
4
+ * Why an inventory at all.
5
+ *
6
+ * `archive/road-to-wired-instruments.md` closed four instances of one defect —
7
+ * an instrument this repository built, tested, and then connected to nothing —
8
+ * one instance at a time, and shipped five hand-written `grep` acceptance
9
+ * criteria instead of a detector. Two further instances were open the whole
10
+ * time and nothing in the tree could notice them. This module is the reader
11
+ * half of the fix: a declared list of loop surfaces and the exported helpers
12
+ * they are supposed to call, plus the one predicate that says whether a
13
+ * declared helper is called by anything at all.
14
+ *
15
+ * What it is NOT.
16
+ *
17
+ * Reader input. Nothing executes from it, no surface changes behaviour because
18
+ * of it, and there is no runner. That is Risk 3 of the roadmap that introduced
19
+ * it, stated as a constraint rather than a hope: the moment something
20
+ * dispatches off this file it has become the loop engine a recorded decision
21
+ * forbids.
22
+ *
23
+ * The reach predicate, and the correction that shaped it.
24
+ *
25
+ * The obvious predicate — "no file other than its own imports this export" —
26
+ * was measured against `loop_guards.ts` and calls nine of its fourteen exports
27
+ * dead. Most of those are wrong: a helper used by its own neighbours in the
28
+ * same file is reached every time that file is. So the criterion is
29
+ * occurrence-based and counts the declaration itself:
30
+ *
31
+ * an export is DEAD when it appears in `src/` exactly once,
32
+ * including its own declaration.
33
+ *
34
+ * Two is enough, deliberately. A second occurrence anywhere in `src/` means
35
+ * some production line names it; whether that line sits in the same file or
36
+ * another one is not a difference this predicate needs to resolve, and
37
+ * resolving it would mean writing a second import resolver.
38
+ *
39
+ * `tests/` is outside `src/` and therefore outside the count by construction —
40
+ * which is the point. A test-only caller is exactly the state both open
41
+ * instances are in.
42
+ *
43
+ * Why `cap` accepts a markdown anchor as well as a symbol.
44
+ *
45
+ * The roadmap step that specified this file asked for `file:symbol` and
46
+ * "never prose". Reproduced against the tree on 2026-09-13, that is
47
+ * unsatisfiable for two of the five surfaces and a category error for both:
48
+ * `verify-repair-loop` and `experiment-loop` are SKILLS, and a skill's bound
49
+ * is a documented knob, not a TypeScript constant. `grep -rn max_attempts
50
+ * src/` returns hits only inside the skill's own directory.
51
+ *
52
+ * The property that requirement was reaching for is preserved exactly:
53
+ * a bound must resolve to something that demonstrably EXISTS. So a `.ts`
54
+ * target must be a defined symbol, a `.md` target must be a literal token
55
+ * present in that file, and a markdown-bound row must additionally declare
56
+ * `bound_kind: prose` — the prose-ness is recorded rather than inferred, and
57
+ * `inventoryVerdict` reports those surfaces as a named set so a prose bound
58
+ * can never be read as equal to a code one.
59
+ */
60
+
61
+ import * as fs from 'node:fs';
62
+ import * as path from 'node:path';
63
+
64
+ import { parse as parseYaml } from 'yaml';
65
+
66
+ export const INVENTORY_REL = 'src/config/loop-surfaces.yaml';
67
+
68
+ /** Fields every surface row must carry. Order is the order they are reported in. */
69
+ export const SURFACE_FIELDS = [
70
+ 'cap',
71
+ 'no_progress',
72
+ 'success_stop',
73
+ 'checker',
74
+ 'human_gate',
75
+ 'terminal_vocabulary',
76
+ 'production_consumers',
77
+ ] as const;
78
+
79
+ export type SurfaceField = (typeof SURFACE_FIELDS)[number];
80
+
81
+ /** A loop surface — a thing that iterates under a bound. */
82
+ export interface LoopSurface {
83
+ id: string;
84
+ /** `code` or `prose` — which kind of artefact the surface itself is. */
85
+ kind: string;
86
+ /** `code` when `cap`/`no_progress` name symbols, `prose` when they name doc anchors. */
87
+ bound_kind: string;
88
+ cap: string;
89
+ no_progress: string;
90
+ success_stop: string;
91
+ checker: string;
92
+ human_gate: string;
93
+ terminal_vocabulary: string;
94
+ production_consumers: string[];
95
+ }
96
+
97
+ /**
98
+ * An exported helper a surface is meant to call.
99
+ *
100
+ * Separated from surfaces because the reach question only applies here: a
101
+ * surface is reached by being bound (a hook slot, a skill invocation), an
102
+ * instrument is reached by being called, and only the second is answerable by
103
+ * counting occurrences of a symbol.
104
+ */
105
+ export interface LoopInstrument {
106
+ id: string;
107
+ file: string;
108
+ role: string;
109
+ /** `experimental` exempts it from the reach requirement — with a deadline. */
110
+ status?: string | undefined;
111
+ /** ISO `YYYY-MM-DD`. Required under `status: experimental`; a past date reds. */
112
+ expires?: string | undefined;
113
+ note?: string | undefined;
114
+ }
115
+
116
+ export interface Inventory {
117
+ surfaces: LoopSurface[];
118
+ instruments: LoopInstrument[];
119
+ }
120
+
121
+ /** `path/to/file.ts:SYMBOL` or `path/to/file.md:ANCHOR`. */
122
+ const BOUND_TARGET = /^([A-Za-z0-9_./-]+\.(?:ts|js|md)):([A-Za-z_$][A-Za-z0-9_$-]*)$/;
123
+
124
+ function str(row: Record<string, unknown>, key: string): string {
125
+ const v = row[key];
126
+ return v === undefined || v === null ? '' : String(v);
127
+ }
128
+
129
+ export function readInventory(root: string): Inventory | null {
130
+ const p = path.join(root, INVENTORY_REL);
131
+ if (!fs.existsSync(p)) return null;
132
+ const doc = parseYaml(fs.readFileSync(p, 'utf-8')) as {
133
+ surfaces?: Record<string, Record<string, unknown>>;
134
+ instruments?: Record<string, Record<string, unknown>>;
135
+ } | null;
136
+
137
+ const surfaces: LoopSurface[] = [];
138
+ for (const [id, row] of Object.entries(doc?.surfaces ?? {})) {
139
+ surfaces.push({
140
+ id,
141
+ kind: str(row, 'kind'),
142
+ bound_kind: str(row, 'bound_kind'),
143
+ cap: str(row, 'cap'),
144
+ no_progress: str(row, 'no_progress'),
145
+ success_stop: str(row, 'success_stop'),
146
+ checker: str(row, 'checker'),
147
+ human_gate: str(row, 'human_gate'),
148
+ terminal_vocabulary: str(row, 'terminal_vocabulary'),
149
+ production_consumers: Array.isArray(row['production_consumers'])
150
+ ? (row['production_consumers'] as unknown[]).map(String)
151
+ : [],
152
+ });
153
+ }
154
+
155
+ const instruments: LoopInstrument[] = [];
156
+ for (const [id, row] of Object.entries(doc?.instruments ?? {})) {
157
+ instruments.push({
158
+ id,
159
+ file: str(row, 'file'),
160
+ role: str(row, 'role'),
161
+ status: row['status'] === undefined ? undefined : String(row['status']),
162
+ expires: row['expires'] === undefined ? undefined : String(row['expires']),
163
+ note: row['note'] === undefined ? undefined : String(row['note']),
164
+ });
165
+ }
166
+
167
+ surfaces.sort((a, b) => a.id.localeCompare(b.id));
168
+ instruments.sort((a, b) => a.id.localeCompare(b.id));
169
+ return { surfaces, instruments };
170
+ }
171
+
172
+ /** Every `.ts` / `.js` file under `src/`, sorted. Cached per root. */
173
+ const sourceCache = new Map<string, string[]>();
174
+
175
+ export function sourceFiles(root: string): string[] {
176
+ const hit = sourceCache.get(root);
177
+ if (hit !== undefined) return hit;
178
+ const out: string[] = [];
179
+ const walk = (dir: string): void => {
180
+ let entries: fs.Dirent[];
181
+ try {
182
+ entries = fs.readdirSync(dir, { withFileTypes: true });
183
+ } catch {
184
+ return;
185
+ }
186
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
187
+ const full = path.join(dir, e.name);
188
+ if (e.isDirectory()) {
189
+ if (e.name === 'node_modules') continue;
190
+ walk(full);
191
+ } else if (e.name.endsWith('.ts') || e.name.endsWith('.js')) {
192
+ out.push(full);
193
+ }
194
+ }
195
+ };
196
+ walk(path.join(root, 'src'));
197
+ sourceCache.set(root, out);
198
+ return out;
199
+ }
200
+
201
+ /** Drop the file-list cache. The self-test builds throwaway trees. */
202
+ export function resetSourceCache(): void {
203
+ sourceCache.clear();
204
+ }
205
+
206
+ function escapeRe(s: string): string {
207
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
208
+ }
209
+
210
+ /**
211
+ * How many times `symbol` occurs anywhere under `src/`, declaration included.
212
+ *
213
+ * Word-bounded, so `stallSignal` does not match `stallSignalled`. Deliberately
214
+ * text-level: a symbol resolver would have to model re-exports, barrel files
215
+ * and type-only imports to beat this, and would then be a second
216
+ * implementation of the compiler. The loose version's failure mode is a count
217
+ * that is too HIGH — reporting a dead export as live, which is the quiet
218
+ * direction. It never manufactures an accusation.
219
+ */
220
+ export function occurrences(root: string, symbol: string): number {
221
+ if (symbol === '') return 0;
222
+ const re = new RegExp(`\\b${escapeRe(symbol)}\\b`, 'g');
223
+ let n = 0;
224
+ for (const f of sourceFiles(root)) {
225
+ n += (fs.readFileSync(f, 'utf-8').match(re) ?? []).length;
226
+ }
227
+ return n;
228
+ }
229
+
230
+ /** Does `file` DEFINE `symbol` (a `.ts` target) or merely contain it (a `.md` anchor)? */
231
+ export function resolvesTarget(root: string, file: string, token: string): boolean {
232
+ const p = path.join(root, file);
233
+ if (!fs.existsSync(p)) return false;
234
+ const text = fs.readFileSync(p, 'utf-8');
235
+ const esc = escapeRe(token);
236
+ if (file.endsWith('.md')) return new RegExp(`\\b${esc}\\b`).test(text);
237
+ const decl = new RegExp(`\\b(?:const|let|var|function|interface|type|class|enum)\\s+${esc}\\b`);
238
+ if (decl.test(text)) return true;
239
+ // Object keys, union members and class fields: `TOKEN:` / `TOKEN(` at line start.
240
+ return new RegExp(`^\\s*(?:readonly\\s+)?${esc}\\s*[:(]`, 'm').test(text);
241
+ }
242
+
243
+ export interface ReachFinding {
244
+ id: string;
245
+ file: string;
246
+ /** `dead` · `expired` · `no-expiry` · `bad-status` — why it reds. */
247
+ reason: string;
248
+ detail: string;
249
+ }
250
+
251
+ export interface ShapeFinding {
252
+ id: string;
253
+ reason: string;
254
+ detail: string;
255
+ }
256
+
257
+ export interface InventoryVerdict {
258
+ /** Surfaces with a missing field, or a bound that resolves to nothing. */
259
+ shape: ShapeFinding[];
260
+ /** Declared instruments with no production consumer and no valid exemption. */
261
+ reach: ReachFinding[];
262
+ /** Instruments reported live — the does-not-over-fire evidence. */
263
+ live: string[];
264
+ /** Instruments held by a valid, unexpired `experimental` exemption. */
265
+ exempt: string[];
266
+ /** Surfaces whose bound is documented prose, not a code symbol. Reported, never red. */
267
+ proseBound: string[];
268
+ }
269
+
270
+ /** An instrument is reached when it occurs more than once in `src/`. */
271
+ export const REACH_MINIMUM_OCCURRENCES = 2;
272
+
273
+ const BOUND_FIELDS = ['cap', 'no_progress'] as const;
274
+
275
+ function checkSurface(root: string, s: LoopSurface, shape: ShapeFinding[]): void {
276
+ for (const f of SURFACE_FIELDS) {
277
+ const v = s[f];
278
+ const empty = Array.isArray(v) ? v.length === 0 : String(v).trim() === '';
279
+ if (empty) shape.push({ id: s.id, reason: 'missing-field', detail: `\`${f}\` is empty` });
280
+ }
281
+ if (s.bound_kind !== 'code' && s.bound_kind !== 'prose') {
282
+ shape.push({
283
+ id: s.id,
284
+ reason: 'bad-bound-kind',
285
+ detail: `\`bound_kind: ${s.bound_kind}\` — must be \`code\` or \`prose\``,
286
+ });
287
+ }
288
+ for (const f of BOUND_FIELDS) {
289
+ const raw = String(s[f]).trim();
290
+ if (raw === '') continue;
291
+ const m = BOUND_TARGET.exec(raw);
292
+ if (m === null) {
293
+ shape.push({
294
+ id: s.id,
295
+ reason: 'not-a-target',
296
+ detail: `\`${f}: ${raw}\` is not \`path/to/file.{ts,js,md}:TOKEN\``,
297
+ });
298
+ continue;
299
+ }
300
+ const [, file, token] = m as unknown as [string, string, string];
301
+ // A markdown bound must SAY it is prose. Undeclared, it would read as a
302
+ // code bound in every report that aggregates this file.
303
+ if (file.endsWith('.md') && s.bound_kind !== 'prose') {
304
+ shape.push({
305
+ id: s.id,
306
+ reason: 'undeclared-prose-bound',
307
+ detail: `\`${f}\` points into ${file} but the row is \`bound_kind: ${s.bound_kind}\``,
308
+ });
309
+ }
310
+ if (!resolvesTarget(root, file, token)) {
311
+ shape.push({
312
+ id: s.id,
313
+ reason: 'unresolved-target',
314
+ detail: `\`${f}\` names ${token} in ${file}, which does not carry it`,
315
+ });
316
+ }
317
+ }
318
+ }
319
+
320
+ function checkInstrument(root: string, i: LoopInstrument, day: string, v: InventoryVerdict): void {
321
+ if (i.status !== undefined && i.status !== 'experimental') {
322
+ v.reach.push({
323
+ id: i.id,
324
+ file: i.file,
325
+ reason: 'bad-status',
326
+ detail: `\`status: ${i.status}\` is not a recognised exemption (only \`experimental\`)`,
327
+ });
328
+ return;
329
+ }
330
+ if (i.status === 'experimental') {
331
+ const exp = (i.expires ?? '').trim();
332
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(exp)) {
333
+ v.reach.push({
334
+ id: i.id,
335
+ file: i.file,
336
+ reason: 'no-expiry',
337
+ detail: 'an `experimental` exemption needs `expires: YYYY-MM-DD`',
338
+ });
339
+ return;
340
+ }
341
+ if (exp <= day) {
342
+ v.reach.push({
343
+ id: i.id,
344
+ file: i.file,
345
+ reason: 'expired',
346
+ detail: `the \`experimental\` exemption lapsed on ${exp} — decide, or move the date with a reason`,
347
+ });
348
+ return;
349
+ }
350
+ v.exempt.push(i.id);
351
+ return;
352
+ }
353
+ const n = occurrences(root, i.id);
354
+ if (n < REACH_MINIMUM_OCCURRENCES) {
355
+ v.reach.push({
356
+ id: i.id,
357
+ file: i.file,
358
+ reason: 'dead',
359
+ detail: `occurs ${String(n)}× in src/ (own declaration included) — nothing calls it`,
360
+ });
361
+ } else {
362
+ v.live.push(i.id);
363
+ }
364
+ }
365
+
366
+ export function inventoryVerdict(root: string, today = new Date()): InventoryVerdict {
367
+ const v: InventoryVerdict = { shape: [], reach: [], live: [], exempt: [], proseBound: [] };
368
+ const inv = readInventory(root);
369
+ if (inv === null) return v;
370
+
371
+ for (const s of inv.surfaces) {
372
+ checkSurface(root, s, v.shape);
373
+ if (s.bound_kind === 'prose') v.proseBound.push(s.id);
374
+ }
375
+ const day = today.toISOString().slice(0, 10);
376
+ for (const i of inv.instruments) checkInstrument(root, i, day, v);
377
+
378
+ v.live.sort();
379
+ v.exempt.sort();
380
+ v.proseBound.sort();
381
+ return v;
382
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The long-run continuity record — `road-to-adversarial-verification-and-long-runs`
3
+ * 9.1.
4
+ *
5
+ * `continuity_writer` already writes a session record. This is the MISSION
6
+ * record: the state a twelve-hour run must survive a restart with, and the two
7
+ * rules about reading it back — never re-ask a closed question, and never resume
8
+ * on an authority snapshot that has since been revoked.
9
+ *
10
+ * **Risk 5 is the reason `restore` is not just a deserialise.** The record
11
+ * carries the grant including `expires` and `revoked_by`. A restore that trusted
12
+ * the snapshot would resume with authority the owner withdrew after it was
13
+ * written — so the snapshot is revalidated against the LEDGER, which is the
14
+ * surface a revocation actually writes to. A record and a ledger that disagree
15
+ * are resolved in favour of the ledger, always.
16
+ *
17
+ * Pure. Reading the ledger and the forge belongs to the caller.
18
+ */
19
+
20
+ /** A decision the mission already settled. Re-asking one is the failure. */
21
+ export interface DecisionRef {
22
+ readonly id: string;
23
+ /** Where the reasoning lives, so a restore can read rather than redo it. */
24
+ readonly where: string;
25
+ }
26
+
27
+ /** The authority the mission is running under. */
28
+ export interface AuthoritySnapshot {
29
+ readonly grant: string;
30
+ /** ISO instant, or `null` for a grant with no expiry. */
31
+ readonly expires: string | null;
32
+ /** Who revoked it, or `null` while it stands. */
33
+ readonly revoked_by: string | null;
34
+ }
35
+
36
+ export interface MissionRecord {
37
+ readonly mission_id: string;
38
+ readonly roadmap: string;
39
+ readonly phase: string;
40
+ readonly completed_steps: readonly string[];
41
+ readonly decisions: readonly DecisionRef[];
42
+ readonly authority: AuthoritySnapshot;
43
+ readonly target_branch: string;
44
+ readonly pr: number | null;
45
+ readonly head_sha: string;
46
+ /** The last CI verdict observed, and the head it was about. */
47
+ readonly last_ci: { readonly verdict: string; readonly head: string } | null;
48
+ readonly recovery_epoch: number;
49
+ readonly attempt_count: number;
50
+ readonly pending_reviews: readonly string[];
51
+ readonly owner_owned_residue: readonly string[];
52
+ }
53
+
54
+ /** Every field a record must carry. Absent means the record is not one. */
55
+ export const REQUIRED_FIELDS: readonly (keyof MissionRecord)[] = [
56
+ 'mission_id',
57
+ 'roadmap',
58
+ 'phase',
59
+ 'completed_steps',
60
+ 'decisions',
61
+ 'authority',
62
+ 'target_branch',
63
+ 'pr',
64
+ 'head_sha',
65
+ 'last_ci',
66
+ 'recovery_epoch',
67
+ 'attempt_count',
68
+ 'pending_reviews',
69
+ 'owner_owned_residue',
70
+ ];
71
+
72
+ /** When the record is written. A side task is NOT one of these. */
73
+ export const WRITE_BOUNDARIES = [
74
+ 'phase-boundary',
75
+ 'stop',
76
+ 'pre-compact',
77
+ 'session-end',
78
+ ] as const;
79
+ export type WriteBoundary = (typeof WRITE_BOUNDARIES)[number];
80
+
81
+ /** Field names missing from a candidate record. Empty means complete. */
82
+ export function missingFields(candidate: Partial<MissionRecord>): string[] {
83
+ return REQUIRED_FIELDS.filter((f) => candidate[f] === undefined).map(String);
84
+ }
85
+
86
+ /** What the LEDGER says about a grant right now. `null` = no ledger entry. */
87
+ export interface LedgerState {
88
+ readonly grant: string;
89
+ readonly revoked_by: string | null;
90
+ }
91
+
92
+ /**
93
+ * `grant-mismatch` covers both ways the two sides can fail to be about one
94
+ * grant: they name different ones, or the record names none at all — which
95
+ * identifies nothing for the ledger to agree with.
96
+ */
97
+ export type RestoreState =
98
+ | 'resume'
99
+ | 'authority-withdrawn'
100
+ | 'authority-expired'
101
+ | 'grant-mismatch';
102
+
103
+ export interface RestoreVerdict {
104
+ readonly state: RestoreState;
105
+ readonly reason: string;
106
+ /** Questions this record already answers. A restore re-asks none of them. */
107
+ readonly closed: readonly string[];
108
+ }
109
+
110
+ /**
111
+ * Decide whether a restored mission may continue.
112
+ *
113
+ * The LEDGER wins over the snapshot, in one direction only: it can revoke a
114
+ * grant the record still shows as live, and it cannot revive one the record
115
+ * shows as revoked. A ledger that could un-revoke would make the record the
116
+ * weaker of two authorities and the revocation advisory.
117
+ *
118
+ * **Identity comes before precedence.** Asking which of the two wins is only
119
+ * meaningful once they are about the same grant. A restore that read
120
+ * `revoked_by` from a ledger describing a DIFFERENT grant would be using the
121
+ * ledger as an oracle for a question it was never asked — a mission authorised
122
+ * under a narrow grant would resume on a later, broader grant's silence. So the
123
+ * identity check runs first and refuses in both directions, which leaves the
124
+ * one-way revoke precedence below exactly as it was.
125
+ */
126
+ export function restore(
127
+ record: MissionRecord,
128
+ ledger: LedgerState | null,
129
+ now: Date,
130
+ ): RestoreVerdict {
131
+ const closed = record.decisions.map((d) => d.id);
132
+
133
+ if (record.authority.grant.trim().length === 0) {
134
+ return {
135
+ state: 'grant-mismatch',
136
+ reason: 'the snapshot names no grant, so there is nothing for the ledger to agree with',
137
+ closed,
138
+ };
139
+ }
140
+ if (ledger !== null && ledger.grant !== record.authority.grant) {
141
+ return {
142
+ state: 'grant-mismatch',
143
+ reason: `the snapshot runs under ${record.authority.grant} and the ledger describes ${ledger.grant}; the two are not about the same grant`,
144
+ closed,
145
+ };
146
+ }
147
+ if (record.authority.revoked_by !== null) {
148
+ return {
149
+ state: 'authority-withdrawn',
150
+ reason: `the record's own snapshot is revoked by ${record.authority.revoked_by}`,
151
+ closed,
152
+ };
153
+ }
154
+ if (ledger !== null && ledger.revoked_by !== null) {
155
+ return {
156
+ state: 'authority-withdrawn',
157
+ reason: `the ledger reports the grant revoked by ${ledger.revoked_by} after the snapshot was written`,
158
+ closed,
159
+ };
160
+ }
161
+ if (record.authority.expires !== null) {
162
+ const at = Date.parse(record.authority.expires);
163
+ // An UNPARSEABLE expiry is treated as expired. A grant whose lifetime
164
+ // cannot be read is not a grant with no lifetime.
165
+ if (!Number.isFinite(at) || at <= now.getTime()) {
166
+ return {
167
+ state: 'authority-expired',
168
+ reason: `the grant's expiry (${record.authority.expires}) is not a future instant`,
169
+ closed,
170
+ };
171
+ }
172
+ }
173
+ return { state: 'resume', reason: 'the grant stands in both the record and the ledger', closed };
174
+ }
175
+
176
+ /**
177
+ * Has this question already been settled by the mission?
178
+ *
179
+ * The whole point of the record: a restore that re-asks a closed question has
180
+ * spent the owner's attention on something the mission already decided, which is
181
+ * the loop this roadmap exists to end.
182
+ */
183
+ export function alreadyAnswered(record: MissionRecord, questionId: string): boolean {
184
+ return record.decisions.some((d) => d.id === questionId);
185
+ }
186
+
187
+ /** A side task does not clear the record. */
188
+ export function clearedBy(boundary: string): boolean {
189
+ return boundary === 'mission-complete';
190
+ }