@event4u/agent-config 15.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 (283) 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 +301 -347
  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 +77 -29
  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 +97 -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/rules/user-interaction.md +31 -30
  40. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  41. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  42. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  44. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  45. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  46. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
  47. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  48. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  51. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  52. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  53. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  54. package/dist/agent-src/templates/agent-settings.md +1 -1
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +188 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  58. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  59. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  60. package/dist/cli-delegate/check_update_banner.js +40 -2
  61. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  62. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  63. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  64. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  65. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  66. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  67. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  68. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  69. package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
  70. package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
  71. package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
  72. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  73. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  74. package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
  75. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  76. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  77. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  78. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  79. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  80. package/dist/cli-delegate/cmd_conformance.js +918 -325
  81. package/dist/cli-delegate/cmd_doctor.js +7 -5
  82. package/dist/cli-delegate/cmd_explain.js +5 -3
  83. package/dist/cli-delegate/cmd_export.js +9 -7
  84. package/dist/cli-delegate/cmd_fleet.js +4 -2
  85. package/dist/cli-delegate/cmd_handoff.js +6 -6
  86. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +3 -2
  89. package/dist/cli-delegate/cmd_preflight.js +4 -2
  90. package/dist/cli-delegate/cmd_prune.js +10 -8
  91. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  92. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  93. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  94. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  95. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  96. package/dist/cli-delegate/cmd_settings_set.js +14 -1
  97. package/dist/cli-delegate/cmd_sync.js +10 -8
  98. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  99. package/dist/cli-delegate/cmd_update.js +1 -1
  100. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  101. package/dist/cli-delegate/cmd_validate.js +12 -10
  102. package/dist/cli-delegate/cmd_versions.js +1 -1
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +87 -40
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +4 -4
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +10 -8
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +8 -6
  112. package/dist/hooks/dispatch.js +2601 -1073
  113. package/dist/install/claudeRuleRewrite.js +14 -3
  114. package/dist/install/conflict.js +60 -10
  115. package/dist/install/conflictTracker.js +218 -0
  116. package/dist/install/fsPrimitives.js +54 -0
  117. package/dist/install/install.mjs +2008 -1481
  118. package/dist/install/manifestFiles.js +259 -0
  119. package/dist/install/preflight.js +14 -2
  120. package/dist/install/preserve.js +157 -0
  121. package/dist/install/recordedOwnership.js +132 -0
  122. package/dist/install/txlog.js +11 -2
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/mcp/server.json +2 -2
  125. package/dist/mcp/server.mjs +1612 -667
  126. package/dist/router.json +1 -1
  127. package/dist/scripts/_lib/agent_settings.js +3 -2
  128. package/dist/scripts/_lib/fs_atomic.js +167 -0
  129. package/dist/scripts/_lib/installed_lock.js +1 -1
  130. package/dist/scripts/_lib/installed_tools.js +460 -0
  131. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  132. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  133. package/dist/server/io/yamlIO.js +50 -0
  134. package/dist/server/routes/install.js +8 -0
  135. package/dist/server/routes/wizard.js +9 -3
  136. package/dist/server/schemas/settings.js +17 -5
  137. package/dist/shared/missionExecution.js +97 -0
  138. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  139. package/dist/ui/index.html +1 -1
  140. package/docs/MIGRATION.md +103 -0
  141. package/docs/contracts/settings-classes.md +26 -7
  142. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  143. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  144. package/docs/guidelines/design-fidelity-routing.md +29 -0
  145. package/package.json +1 -1
  146. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  147. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  148. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  149. package/src/config/agent-settings.template.yml +139 -27
  150. package/src/config/auto-rule-budget.json +16 -0
  151. package/src/config/ci-local-parity.yml +18 -5
  152. package/src/config/continuity-surface.json +28 -0
  153. package/src/config/gate-coverage.yml +146 -13
  154. package/src/config/gate-violation-baselines.json +6 -6
  155. package/src/config/hook-token-budget.json +2 -0
  156. package/src/config/host-injection-effect.json +1 -1
  157. package/src/config/loop-surfaces.yaml +215 -0
  158. package/src/config/platform-anchor.json +3 -2
  159. package/src/config/preamble-payload-budget.json +12 -7
  160. package/src/config/preamble-payload-exceptions.json +28 -0
  161. package/src/config/release-gate-locality.yml +11 -0
  162. package/src/config/routing-coverage-seed.json +18 -4
  163. package/src/config/rule-enforcement-baseline.json +0 -1
  164. package/src/install/claudeRuleRewrite.ts +16 -2
  165. package/src/install/conflict.ts +65 -9
  166. package/src/install/conflictTracker.ts +299 -0
  167. package/src/install/fsPrimitives.ts +54 -0
  168. package/src/install/manifestFiles.ts +312 -0
  169. package/src/install/preflight.ts +16 -2
  170. package/src/install/preserve.ts +205 -0
  171. package/src/install/recordedOwnership.ts +149 -0
  172. package/src/install/txlog.ts +11 -2
  173. package/src/install/types.ts +19 -4
  174. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  175. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  176. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  177. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  178. package/src/scripts/_cli/doctor_execution.ts +90 -0
  179. package/src/scripts/_dispatch.bash +3 -1
  180. package/src/scripts/_lib/activation_payload.ts +247 -0
  181. package/src/scripts/_lib/agent_settings.ts +4 -2
  182. package/src/scripts/_lib/authority_path.ts +102 -0
  183. package/src/scripts/_lib/base_ref_payload.ts +315 -0
  184. package/src/scripts/_lib/cascade_base.ts +94 -0
  185. package/src/scripts/_lib/changelog_eras.ts +7 -2
  186. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  187. package/src/scripts/_lib/council_transport.ts +129 -0
  188. package/src/scripts/_lib/delivery_ready.ts +112 -0
  189. package/src/scripts/_lib/dropped_decision.ts +142 -0
  190. package/src/scripts/_lib/forge_protection.ts +267 -0
  191. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  192. package/src/scripts/_lib/installed_lock.ts +1 -1
  193. package/src/scripts/_lib/json_python_parity.ts +127 -0
  194. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  195. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  196. package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
  197. package/src/scripts/_lib/mission_record.ts +190 -0
  198. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  199. package/src/scripts/_lib/obligations.ts +402 -0
  200. package/src/scripts/_lib/orchestration_record.ts +11 -20
  201. package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
  202. package/src/scripts/_lib/release_findings_ingest.ts +324 -0
  203. package/src/scripts/_lib/release_highlights.ts +199 -10
  204. package/src/scripts/_lib/release_holds.ts +493 -0
  205. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  206. package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
  207. package/src/scripts/_lib/review_baseline.ts +254 -0
  208. package/src/scripts/_lib/review_skipped_record.ts +61 -1
  209. package/src/scripts/_lib/rides_along.ts +102 -0
  210. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  211. package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
  212. package/src/scripts/_lib/test_delta.ts +166 -0
  213. package/src/scripts/_lib/test_provenance.ts +171 -0
  214. package/src/scripts/_lib/turn_end_refusals.ts +51 -4
  215. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  216. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  217. package/src/scripts/ai_council/config.ts +8 -11
  218. package/src/scripts/ai_council/fallback_config.ts +5 -3
  219. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  220. package/src/scripts/ask_block_census.ts +285 -15
  221. package/src/scripts/check_beta_review_markers.ts +35 -4
  222. package/src/scripts/check_enforcement_matrix.ts +624 -0
  223. package/src/scripts/check_finding_dispositions.ts +167 -16
  224. package/src/scripts/check_gate_reachability.ts +173 -10
  225. package/src/scripts/check_generator_sync.ts +954 -0
  226. package/src/scripts/check_held_object_arrivals.ts +381 -0
  227. package/src/scripts/check_preamble_payload_budget.ts +474 -124
  228. package/src/scripts/check_release_highlights.ts +33 -1
  229. package/src/scripts/check_release_holds.ts +567 -0
  230. package/src/scripts/check_release_published.ts +1 -1
  231. package/src/scripts/check_reply_consistency.ts +86 -17
  232. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  233. package/src/scripts/check_test_delta.ts +294 -0
  234. package/src/scripts/check_test_weakening.ts +299 -0
  235. package/src/scripts/closure_scan.ts +333 -0
  236. package/src/scripts/compile_hook_manifest.ts +43 -3
  237. package/src/scripts/council_record_shape.ts +209 -0
  238. package/src/scripts/hook_manifest.json +1 -1
  239. package/src/scripts/hook_manifest.yaml +127 -12
  240. package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
  241. package/src/scripts/hooks/concern_registry.ts +6 -0
  242. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  243. package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
  244. package/src/scripts/hooks/evidence_independence.ts +96 -0
  245. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  246. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  247. package/src/scripts/hooks/review_baseline_hook.ts +130 -0
  248. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  249. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  250. package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
  251. package/src/scripts/hooks_doctor.ts +89 -1
  252. package/src/scripts/hooks_status.ts +19 -0
  253. package/src/scripts/install.ts +107 -207
  254. package/src/scripts/interruption_report.ts +8 -2
  255. package/src/scripts/janitor.ts +15 -1
  256. package/src/scripts/lint_decision_classes.ts +464 -0
  257. package/src/scripts/lint_hook_manifest.ts +11 -0
  258. package/src/scripts/lint_major_migration_sections.ts +450 -0
  259. package/src/scripts/lint_plan_risk_register.ts +47 -3
  260. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  261. package/src/scripts/lint_roadmap_producers.ts +202 -0
  262. package/src/scripts/measure_rule_budget.ts +101 -5
  263. package/src/scripts/measure_turn_end_gate.ts +302 -5
  264. package/src/scripts/new_roadmap.ts +37 -0
  265. package/src/scripts/orchestration_record.ts +0 -5
  266. package/src/scripts/release.ts +66 -162
  267. package/src/scripts/release_drill.ts +460 -15
  268. package/src/scripts/release_env.ts +1 -1
  269. package/src/scripts/release_publication.ts +52 -1
  270. package/src/scripts/report_enforcement_drift.ts +566 -0
  271. package/src/scripts/report_held_object_arrivals.ts +456 -0
  272. package/src/scripts/report_host_injection_effect.ts +3 -11
  273. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  274. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  275. package/src/scripts/schemas/command.schema.json +4 -0
  276. package/src/scripts/scope_growth.ts +219 -0
  277. package/src/scripts/sync_agent_settings.ts +140 -18
  278. package/src/scripts/trigger_eval_grandfather.json +0 -1
  279. package/src/scripts/ui_conformance_probe.ts +622 -0
  280. package/src/server/io/yamlIO.ts +48 -0
  281. package/src/server/schemas/settings.ts +31 -7
  282. package/src/shared/missionExecution.ts +126 -0
  283. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -103,6 +103,121 @@ export interface Ledger {
103
103
  findings: LedgerFinding[];
104
104
  /** Why the finding set is empty. Required when it is; see empty_ledger_problem. */
105
105
  no_findings_reason?: string;
106
+ // The six integrity fields, declared rather than swept into an index
107
+ // signature. An index signature would hold them, and would also disable
108
+ // excess-property and typo detection on the whole type for every consumer:
109
+ // under `--strict`, `l.findigs` type-checks against a Ledger carrying one.
110
+ // The set is known and finite, so it costs six lines to keep the type.
111
+ review_independence?: string;
112
+ context_relation?: string;
113
+ acceptance_status?: string;
114
+ assurance?: string;
115
+ reviewers?: string[];
116
+ coverage?: unknown;
117
+ }
118
+
119
+ /**
120
+ * Fields the self-review artifact carries that say how much a ledger's silence
121
+ * is worth, and which `--ingest` used to drop on the floor.
122
+ *
123
+ * `check_review_schema` derives `acceptance_status` and `assurance` from
124
+ * `review_independence` and fails a ledger whose declarations disagree — so an
125
+ * ingested ledger missing all three was not merely thinner than a hand-written
126
+ * one, it could not satisfy the gate that reads it. Every committed ledger from
127
+ * 14.19.0 onward carries the full set; only the ingest path produced files
128
+ * without it, which is one of the two reasons nobody could run the ingest and
129
+ * get a green tree.
130
+ *
131
+ * Carried rather than recomputed: the producing run is the only thing that
132
+ * knows how many reviewers answered and how much of the diff they saw, and a
133
+ * consumer inferring it from the findings would be inventing the integrity
134
+ * claim the fields exist to record.
135
+ */
136
+ export const INTEGRITY_FIELDS = [
137
+ 'review_independence',
138
+ 'context_relation',
139
+ 'acceptance_status',
140
+ 'assurance',
141
+ 'reviewers',
142
+ 'coverage',
143
+ ] as const;
144
+
145
+ /**
146
+ * Merge a self-review artifact into a ledger: new findings by id, the integrity
147
+ * fields on first write, and — when the review found nothing — the reason that
148
+ * makes an empty ledger legible.
149
+ *
150
+ * MUTATES AND RETURNS THE SAME OBJECT. Callers must use the returned `ledger`
151
+ * rather than relying on the mutation; the two are the same reference today and
152
+ * the signature is what a later reader will trust.
153
+ *
154
+ * The integrity fields are written only when the ledger does not already carry
155
+ * them. A second ingest into a ledger a human has since adjudicated must not
156
+ * silently restate what the earlier run claimed about independence — the first
157
+ * producing run is the one whose claim the dispositions were filled against.
158
+ *
159
+ * The empty-findings case is the one that used to deadlock a release. A clean
160
+ * review produces `findings: []`, `empty_ledger_problem` refuses an empty
161
+ * ledger with no `no_findings_reason`, and the release then stopped demanding
162
+ * dispositions for zero findings — an instruction nobody can follow. An ingest
163
+ * knows exactly what a clean review means and writes it, with the coverage
164
+ * numbers as the checkable part rather than a bare assurance.
165
+ */
166
+ export function merge_ingest(
167
+ ledger: Ledger,
168
+ artifact: Record<string, unknown>,
169
+ ): { ledger: Ledger; added: number; carried: string[]; reasoned: boolean } {
170
+ const known = new Set(ledger.findings.map((f) => f.finding_id));
171
+ let added = 0;
172
+ for (const f of (artifact['findings'] as LedgerFinding[] | undefined) ?? []) {
173
+ if (!known.has(f.finding_id)) {
174
+ ledger.findings.push(f);
175
+ added++;
176
+ }
177
+ }
178
+ // Assigned through one `unknown` view rather than per field: the six are a
179
+ // closed set the type declares, and spelling out six near-identical
180
+ // if-blocks to satisfy the checker would make the set harder to extend than
181
+ // the type is to read. The view is local to this loop.
182
+ const carried: string[] = [];
183
+ const sink = ledger as unknown as Record<string, unknown>;
184
+ for (const key of INTEGRITY_FIELDS) {
185
+ if (sink[key] === undefined && artifact[key] !== undefined) {
186
+ sink[key] = artifact[key];
187
+ carried.push(key);
188
+ }
189
+ }
190
+ // Only on an artifact that actually SAYS it found nothing. Without the key
191
+ // check, any JSON object without a `findings` key reads as an empty finding
192
+ // set, and the sentence below would assert a review that never happened —
193
+ // into the permanent record, which is worse than the deadlock it replaces.
194
+ let reasoned = false;
195
+ const declaresFindings = Array.isArray(artifact['findings']);
196
+ if (
197
+ declaresFindings &&
198
+ ledger.findings.length === 0 &&
199
+ (ledger.no_findings_reason ?? '').trim() === ''
200
+ ) {
201
+ ledger.no_findings_reason = _clean_review_reason(artifact);
202
+ reasoned = true;
203
+ }
204
+ return { ledger, added, carried, reasoned };
205
+ }
206
+
207
+ /** The `no_findings_reason` an ingest of a zero-findings artifact writes. */
208
+ function _clean_review_reason(artifact: Record<string, unknown>): string {
209
+ const cov = artifact['coverage'] as Record<string, unknown> | undefined;
210
+ const reviewed = cov?.['filesReviewed'];
211
+ const total = cov?.['filesTotal'];
212
+ const scope =
213
+ typeof reviewed === 'number' && typeof total === 'number'
214
+ ? ` over ${reviewed} of ${total} changed file(s)`
215
+ : '';
216
+ return (
217
+ `The self-review ran and reported no findings${scope}; ingested from its own ` +
218
+ 'artifact by the release, so the empty set is a result rather than an absence. ' +
219
+ 'Check it against the coverage block above and the run that produced it.'
220
+ );
106
221
  }
107
222
 
108
223
  /** Mirrors self_review_gate.classifyBlocking — security/claim × critical/high. */
@@ -389,32 +504,68 @@ function main(argv: readonly string[]): number {
389
504
  const ledgerPath = _ledger_path(dir, release);
390
505
 
391
506
  if (ingest) {
392
- const incoming = parse_ledger(
393
- JSON.stringify({
394
- schema_version: 1,
395
- release,
396
- findings: (JSON.parse(fs.readFileSync(ingest, 'utf-8')) as { findings: LedgerFinding[] }).findings,
397
- }),
398
- ingest,
399
- );
507
+ // Guarded: a truncated or corrupt artifact otherwise exits the child on
508
+ // an uncaught exception, and the release reports only "command failed"
509
+ // with no indication that the artifact is the problem.
510
+ let artifact: Record<string, unknown>;
511
+ try {
512
+ artifact = JSON.parse(fs.readFileSync(ingest, 'utf-8')) as Record<string, unknown>;
513
+ } catch (e) {
514
+ process.stderr.write(
515
+ `❌ ${ingest}: not readable as JSON (${e instanceof Error ? e.message : String(e)})\n`,
516
+ );
517
+ return 2;
518
+ }
519
+ // Run the incoming findings through the parser the committed ledger goes
520
+ // through, and keep the RESULT rather than discarding it. What that buys
521
+ // is narrow and worth stating: the parser checks the envelope and that
522
+ // every entry has a non-empty `finding_id`. It does NOT check severity,
523
+ // kind or title, so an entry with a bad severity still lands — the
524
+ // schema gate is what reads those, on the committed file.
525
+ let incoming: Ledger;
526
+ try {
527
+ incoming = parse_ledger(
528
+ JSON.stringify({
529
+ schema_version: 1,
530
+ release,
531
+ findings: (artifact['findings'] as LedgerFinding[] | undefined) ?? [],
532
+ }),
533
+ ingest,
534
+ );
535
+ } catch (e) {
536
+ process.stderr.write(`❌ ${e instanceof Error ? e.message : String(e)}\n`);
537
+ return 2;
538
+ }
400
539
  let ledger: Ledger = { schema_version: 1, release, findings: [] };
401
540
  if (fs.existsSync(ledgerPath)) {
402
541
  ledger = parse_ledger(fs.readFileSync(ledgerPath, 'utf-8'), ledgerPath);
403
542
  }
404
- const known = new Set(ledger.findings.map((f) => f.finding_id));
405
- let added = 0;
406
- for (const f of incoming.findings) {
407
- if (!known.has(f.finding_id)) {
408
- ledger.findings.push(f);
409
- added++;
410
- }
411
- }
543
+ const merged = merge_ingest(ledger, { ...artifact, findings: incoming.findings });
544
+ ledger = merged.ledger;
545
+ const { added, carried, reasoned } = merged;
412
546
  fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
413
547
  fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n');
414
548
  process.stdout.write(
415
549
  `📥 ingested ${added} new finding(s) into ${path.relative(REPO_ROOT, ledgerPath)} — ` +
416
550
  'fill dispositions before the release validation goes green\n',
417
551
  );
552
+ if (carried.length > 0) {
553
+ process.stdout.write(` carried integrity fields: ${carried.join(', ')}\n`);
554
+ }
555
+ if (reasoned) {
556
+ process.stdout.write(
557
+ ' the review reported nothing — wrote no_findings_reason so the empty ' +
558
+ 'ledger is a result rather than an absence\n',
559
+ );
560
+ }
561
+ const absent = INTEGRITY_FIELDS.filter((k) => ledger[k] === undefined);
562
+ if (absent.length > 0) {
563
+ process.stdout.write(
564
+ `⚠️ artifact carried no ${absent.join(', ')} — ` +
565
+ 'check_review_schema derives acceptance_status and assurance from ' +
566
+ 'review_independence and will red on a ledger that declares neither\n',
567
+ );
568
+ }
418
569
  return 0;
419
570
  }
420
571
 
@@ -36,6 +36,12 @@ import * as os from 'node:os';
36
36
 
37
37
  import { reportScanned } from './_lib/scan_scope.js';
38
38
  import { runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
39
+ import {
40
+ INVENTORY_REL,
41
+ inventoryVerdict,
42
+ resetSourceCache,
43
+ type InventoryVerdict,
44
+ } from './_lib/loop_surfaces.js';
39
45
 
40
46
  const REPO_ROOT = process.cwd();
41
47
  const TASKFILES_DIR = 'taskfiles';
@@ -238,6 +244,23 @@ export interface GateVerdict {
238
244
  unreasoned: string[];
239
245
  /** Recorded exemptions that are no longer unreachable — stale rows. */
240
246
  stale: string[];
247
+ /**
248
+ * The `_lib-export-reach` axis: a declared loop instrument nothing calls.
249
+ *
250
+ * A second axis in this one script rather than a second gate, because it
251
+ * asks the same question one layer down. The first axis asks whether CI
252
+ * reaches a gate TARGET; this asks whether production code reaches a
253
+ * declared INSTRUMENT. A separate gate would need its own registration,
254
+ * its own coverage row and its own wiring — and would then be the thing
255
+ * this family exists to detect if any of that were missed.
256
+ *
257
+ * Deliberately scoped to the instruments named in `loop-surfaces.yaml`
258
+ * rather than to every `_lib` export. Measured on `loop_guards.ts`, the
259
+ * unscoped version reports nine of fourteen exports dead; a gate whose
260
+ * signal is a minority of its own output gets allowlisted rather than
261
+ * answered.
262
+ */
263
+ inventory: InventoryVerdict;
241
264
  }
242
265
 
243
266
  /**
@@ -255,25 +278,51 @@ export function gateVerdict(root = REPO_ROOT): GateVerdict {
255
278
  return {
256
279
  unreasoned: r.unreachable.filter((n) => !exempt.has(n)).sort(),
257
280
  stale: [...exempt.keys()].filter((n) => !unreachable.has(n)).sort(),
281
+ inventory: inventoryVerdict(root),
258
282
  };
259
283
  }
260
284
 
285
+ /** The `_lib-export-reach` half of the verdict, written to stderr. */
286
+ function reportInventory(inv: InventoryVerdict): void {
287
+ for (const f of inv.shape) {
288
+ process.stderr.write(
289
+ `❌ ${INVENTORY_REL}: surface \`${f.id}\` — ${f.detail} (${f.reason}). Every surface ` +
290
+ 'carries every field, and a bound resolves to something that exists.\n',
291
+ );
292
+ }
293
+ for (const f of inv.reach) {
294
+ const fix =
295
+ f.reason === 'dead'
296
+ ? 'Give it a production consumer, declare it `status: experimental` with a future ' +
297
+ '`expires:`, or delete it. A built instrument nothing calls is not built.'
298
+ : 'An exemption is a dated deferral, not a label. Decide it, or move the date and ' +
299
+ 'say why in `note:`.';
300
+ process.stderr.write(`❌ ${f.id} (${f.file}) — ${f.detail}. ${fix}\n`);
301
+ }
302
+ }
303
+
261
304
  function runGate(root: string): number {
262
305
  const v = gateVerdict(root);
263
306
  const exempt = readExemptions(root);
307
+ const inv = v.inventory;
308
+ const declared = inv.live.length + inv.exempt.length + inv.reach.length;
264
309
  reportScanned({
265
310
  gate: 'check_gate_reachability',
266
- scanned: exempt.size + analyse(root).unreachable.length,
267
- units: 'exemption row(s) + unreachable target(s)',
268
- roots: [EXEMPTIONS_REL, ROOT_TASKFILE, TASKFILES_DIR],
311
+ scanned: exempt.size + analyse(root).unreachable.length + declared,
312
+ units: 'exemption row(s) + unreachable target(s) + declared loop instrument(s)',
313
+ roots: [EXEMPTIONS_REL, INVENTORY_REL, ROOT_TASKFILE, TASKFILES_DIR],
269
314
  });
270
- if (v.unreasoned.length === 0 && v.stale.length === 0) {
315
+ const inventoryClean = inv.shape.length === 0 && inv.reach.length === 0;
316
+ if (v.unreasoned.length === 0 && v.stale.length === 0 && inventoryClean) {
271
317
  process.stdout.write(
272
318
  `✅ check_gate_reachability: every unreachable gate target carries a recorded reason ` +
273
- `(${String(exempt.size)} exempt).\n`,
319
+ `(${String(exempt.size)} exempt); every declared loop instrument is called or ` +
320
+ `dated (${String(inv.live.length)} live, ${String(inv.exempt.length)} experimental` +
321
+ `${inv.proseBound.length > 0 ? `, ${String(inv.proseBound.length)} prose-bound surface(s)` : ''}).\n`,
274
322
  );
275
323
  return 0;
276
324
  }
325
+ reportInventory(inv);
277
326
  for (const n of v.unreasoned) {
278
327
  process.stderr.write(
279
328
  `❌ ${n} is gate-shaped, unreachable from \`task ci\` and every workflow, and carries ` +
@@ -320,6 +369,58 @@ function selfTest(): number {
320
369
  return v.unreasoned.length === 0 && v.stale.length === 0 ? 0 : 1;
321
370
  };
322
371
 
372
+ /**
373
+ * A throwaway tree for the `_lib-export-reach` axis.
374
+ *
375
+ * `surface` and `instrument` are written verbatim into the inventory, so a
376
+ * case can plant a malformed row without the builder silently repairing
377
+ * it — which is the whole point of a planted failure.
378
+ */
379
+ const inventoryFixture = (opts: { surface: string; instrument: string; calls: boolean }): number => {
380
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gr-inv-'));
381
+ roots.push(dir);
382
+ fs.mkdirSync(path.join(dir, 'src', 'config'), { recursive: true });
383
+ fs.mkdirSync(path.join(dir, 'src', 'lib'), { recursive: true });
384
+ fs.writeFileSync(
385
+ path.join(dir, 'src', 'lib', 'thing.ts'),
386
+ [
387
+ 'export const CEILING = 3;',
388
+ 'export function stalled(): boolean { return false; }',
389
+ 'export function helper(): number { return CEILING; }',
390
+ ...(opts.calls ? ['export const used = helper();'] : []),
391
+ ].join('\n'),
392
+ 'utf-8',
393
+ );
394
+ fs.writeFileSync(
395
+ path.join(dir, 'src', 'config', 'loop-surfaces.yaml'),
396
+ `surfaces:\n${opts.surface}\ninstruments:\n${opts.instrument}\n`,
397
+ 'utf-8',
398
+ );
399
+ resetSourceCache();
400
+ const v = inventoryVerdict(dir);
401
+ resetSourceCache();
402
+ return v.shape.length === 0 && v.reach.length === 0 ? 0 : 1;
403
+ };
404
+
405
+ const wholeSurface = [
406
+ ' s:',
407
+ ' kind: code',
408
+ ' bound_kind: code',
409
+ ' cap: src/lib/thing.ts:CEILING',
410
+ ' no_progress: src/lib/thing.ts:stalled',
411
+ ' success_stop: a stated stop',
412
+ ' checker: a stated checker',
413
+ ' human_gate: none',
414
+ ' terminal_vocabulary: none',
415
+ ' production_consumers: [src/lib/thing.ts]',
416
+ ].join('\n');
417
+ // Identical but for the one missing field — so a red can only be the cap.
418
+ const cappedSurface = wholeSurface
419
+ .split('\n')
420
+ .filter((l) => !l.includes('cap:'))
421
+ .join('\n');
422
+ const plainInstrument = [' helper:', ' file: src/lib/thing.ts', ' role: a stated role'].join('\n');
423
+
323
424
  const cases: SelfTestCase[] = [
324
425
  {
325
426
  name: 'a wired gate target → accept',
@@ -343,9 +444,63 @@ function selfTest(): number {
343
444
  expect: 'reject',
344
445
  run: () => fixture({ target: 'check-x', wired: true, exempt: { 'check-x': 'stale' } }),
345
446
  },
447
+ // The `_lib-export-reach` axis. The accept case comes first so a
448
+ // builder that reds on everything is caught before the three planted
449
+ // failures can be read as the axis working.
450
+ {
451
+ name: 'a whole surface and a called instrument → accept',
452
+ expect: 'accept',
453
+ run: () =>
454
+ inventoryFixture({ surface: wholeSurface, instrument: plainInstrument, calls: true }),
455
+ },
456
+ {
457
+ name: 'a surface with no `cap` → reject',
458
+ expect: 'reject',
459
+ run: () =>
460
+ inventoryFixture({ surface: cappedSurface, instrument: plainInstrument, calls: true }),
461
+ },
462
+ {
463
+ name: 'an instrument with no consumer → reject',
464
+ expect: 'reject',
465
+ run: () =>
466
+ inventoryFixture({ surface: wholeSurface, instrument: plainInstrument, calls: false }),
467
+ },
468
+ {
469
+ // The reason `expires:` is a required field rather than a nicety:
470
+ // a declared-inactive instrument with no deadline is the same dead
471
+ // code wearing a label.
472
+ name: 'an exception whose `expires:` has passed → reject',
473
+ expect: 'reject',
474
+ run: () =>
475
+ inventoryFixture({
476
+ surface: wholeSurface,
477
+ instrument: `${plainInstrument}\n status: experimental\n expires: '2020-01-01'`,
478
+ calls: false,
479
+ }),
480
+ },
481
+ {
482
+ name: 'an `experimental` exception with no `expires:` → reject',
483
+ expect: 'reject',
484
+ run: () =>
485
+ inventoryFixture({
486
+ surface: wholeSurface,
487
+ instrument: `${plainInstrument}\n status: experimental`,
488
+ calls: false,
489
+ }),
490
+ },
491
+ {
492
+ name: 'an unexpired `experimental` exception on a dead instrument → accept',
493
+ expect: 'accept',
494
+ run: () =>
495
+ inventoryFixture({
496
+ surface: wholeSurface,
497
+ instrument: `${plainInstrument}\n status: experimental\n expires: '2099-01-01'`,
498
+ calls: false,
499
+ }),
500
+ },
346
501
  ];
347
502
  try {
348
- return runSelfTest({ gate: 'check_gate_reachability', cases, minCases: 4, minRejectCases: 2 });
503
+ return runSelfTest({ gate: 'check_gate_reachability', cases, minCases: 10, minRejectCases: 6 });
349
504
  } finally {
350
505
  for (const d of roots) fs.rmSync(d, { recursive: true, force: true });
351
506
  }
@@ -355,18 +510,20 @@ export function main(argv: string[] = process.argv.slice(2), root = REPO_ROOT):
355
510
  if (argv.includes('--self-test')) return selfTest();
356
511
  if (argv.includes('--gate')) return runGate(root);
357
512
  const r = analyse(root);
513
+ const inv = inventoryVerdict(root);
358
514
  if (argv.includes('--json')) {
359
- process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
515
+ process.stdout.write(`${JSON.stringify({ ...r, inventory: inv }, null, 2)}\n`);
360
516
  return 0;
361
517
  }
362
518
  // Emitted on the DEFAULT invocation too, not only under `--gate`: the
363
519
  // coverage gate reads a bare run, and a gate that reports what it inspected
364
520
  // only in one mode is a gate that reports nothing in the other.
521
+ const declared = inv.live.length + inv.exempt.length + inv.reach.length;
365
522
  reportScanned({
366
523
  gate: 'check_gate_reachability',
367
- scanned: readExemptions(root).size + r.unreachable.length,
368
- units: 'exemption row(s) + unreachable target(s)',
369
- roots: [EXEMPTIONS_REL, ROOT_TASKFILE, TASKFILES_DIR],
524
+ scanned: readExemptions(root).size + r.unreachable.length + declared,
525
+ units: 'exemption row(s) + unreachable target(s) + declared loop instrument(s)',
526
+ roots: [EXEMPTIONS_REL, INVENTORY_REL, ROOT_TASKFILE, TASKFILES_DIR],
370
527
  });
371
528
  const total = r.reachable.length + r.scriptInWorkflow.length + r.unreachable.length;
372
529
  process.stdout.write(
@@ -375,6 +532,12 @@ export function main(argv: string[] = process.argv.slice(2), root = REPO_ROOT):
375
532
  `UNREACHABLE ${String(r.unreachable.length)}\n`,
376
533
  );
377
534
  for (const n of r.unreachable) process.stdout.write(` · ${n}\n`);
535
+ process.stdout.write(
536
+ `declared loop instruments: ${String(declared)} · live ${String(inv.live.length)} · ` +
537
+ `experimental ${String(inv.exempt.length)} · DEAD ${String(inv.reach.length)} · ` +
538
+ `prose-bound surfaces ${String(inv.proseBound.length)}\n`,
539
+ );
540
+ for (const f of inv.reach) process.stdout.write(` · ${f.id} — ${f.detail}\n`);
378
541
  return 0;
379
542
  }
380
543