@event4u/agent-config 14.18.0 → 14.20.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 (210) 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 +120 -129
  5. package/README.md +71 -149
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +44 -43
  8. package/dist/agent-src/commands/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  17. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  18. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  21. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  22. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  23. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  24. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  25. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  26. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  27. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  28. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  29. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  30. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  31. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  32. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  33. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  34. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  35. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  36. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  37. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  38. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  39. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  40. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  41. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  42. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  43. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  44. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  45. package/dist/agent-src/templates/roadmaps.md +14 -1
  46. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  47. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  48. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  49. package/dist/cli/commands/recordTriggerEval.js +2 -2
  50. package/dist/cli/main.js +1 -1
  51. package/dist/cli/mcp/dispatch.js +10 -0
  52. package/dist/cli/mcp/stdio.js +33 -2
  53. package/dist/cli/mcp/telemetry.js +227 -0
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
  55. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  56. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
  57. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
  58. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  59. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  60. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  61. package/dist/cli-delegate/cmd_conformance.js +489 -121
  62. package/dist/cli-delegate/cmd_doctor.js +2 -2
  63. package/dist/cli-delegate/cmd_export.js +2 -2
  64. package/dist/cli-delegate/cmd_handoff.js +42 -12
  65. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  66. package/dist/cli-delegate/cmd_prune.js +2 -2
  67. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  68. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  69. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +2 -2
  71. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  72. package/dist/cli-delegate/cmd_validate.js +2 -2
  73. package/dist/discovery/deprecation-report.md +1 -1
  74. package/dist/discovery/discovery-manifest.json +34 -33
  75. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  76. package/dist/discovery/discovery-manifest.summary.md +1 -1
  77. package/dist/discovery/dormancy-report.md +1 -1
  78. package/dist/discovery/orphan-report.md +1 -1
  79. package/dist/discovery/packs.json +2 -2
  80. package/dist/discovery/trust-report.md +1 -1
  81. package/dist/discovery/workspaces.json +2 -2
  82. package/dist/hooks/dispatch.js +4455 -3025
  83. package/dist/install/install.mjs +975 -689
  84. package/dist/mcp/registry-manifest.json +2 -2
  85. package/dist/mcp/server.json +2 -2
  86. package/dist/mcp/server.mjs +2087 -1134
  87. package/dist/scripts/_lib/model_tier.js +1 -1
  88. package/dist/scripts/mcp_server/telemetry.js +148 -0
  89. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  90. package/docs/guidelines/agent-infra/naming.md +1 -1
  91. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  92. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  93. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  94. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  95. package/docs/guidelines/design-antipatterns.md +87 -64
  96. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  97. package/package.json +1 -1
  98. package/src/agent-src/README.md +1 -1
  99. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  100. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  101. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  102. package/src/config/assurance-capability-registry.json +9 -0
  103. package/src/config/ci-local-parity.yml +6 -0
  104. package/src/config/gate-coverage.yml +225 -0
  105. package/src/config/gate-violation-baselines.json +18 -6
  106. package/src/config/release-gate-locality.yml +12 -0
  107. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  108. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  109. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  110. package/src/scripts/_cli/cmd_update.ts +1 -1
  111. package/src/scripts/_cli/cmd_versions.ts +1 -1
  112. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  113. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  114. package/src/scripts/_lib/consequence_objects.ts +152 -0
  115. package/src/scripts/_lib/exec_evidence.ts +7 -0
  116. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  117. package/src/scripts/_lib/host_capability.ts +38 -0
  118. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  119. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  120. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  121. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  122. package/src/scripts/_lib/model_tier.ts +1 -1
  123. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  124. package/src/scripts/_lib/orchestration_record.ts +23 -1
  125. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  126. package/src/scripts/_lib/release_highlights.ts +93 -0
  127. package/src/scripts/_lib/release_material.ts +64 -1
  128. package/src/scripts/_lib/release_position.ts +96 -0
  129. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  130. package/src/scripts/_lib/runtime_journal.ts +26 -3
  131. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  132. package/src/scripts/_lib/security_lint.ts +179 -7
  133. package/src/scripts/_lib/self_repair.ts +141 -6
  134. package/src/scripts/_lib/self_repair_store.ts +273 -7
  135. package/src/scripts/_lib/session_register.ts +11 -6
  136. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  137. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  138. package/src/scripts/_lib/surface.ts +86 -0
  139. package/src/scripts/_lib/ui_authority.ts +116 -0
  140. package/src/scripts/adr/regenerate_index.ts +54 -1
  141. package/src/scripts/adr_cite_check.ts +71 -15
  142. package/src/scripts/ai_council/clients.ts +1 -1
  143. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  144. package/src/scripts/build_proof.ts +15 -2
  145. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  146. package/src/scripts/check_beta_review_markers.ts +141 -17
  147. package/src/scripts/check_enforcement_coverage.ts +1 -1
  148. package/src/scripts/check_enforcement_denominator.ts +121 -11
  149. package/src/scripts/check_finding_dispositions.ts +66 -9
  150. package/src/scripts/check_gate_coverage.ts +96 -0
  151. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  152. package/src/scripts/check_pack_size.ts +321 -3
  153. package/src/scripts/check_release_highlights.ts +74 -1
  154. package/src/scripts/check_skill_activation_claim.ts +179 -0
  155. package/src/scripts/check_trigger_evals.ts +1 -1
  156. package/src/scripts/conformance_scan.ts +44 -3
  157. package/src/scripts/explain_run.ts +1 -1
  158. package/src/scripts/generate_capability_matrix.ts +1 -1
  159. package/src/scripts/git_authorization_hook.ts +75 -1
  160. package/src/scripts/handoff_context_hook.ts +54 -7
  161. package/src/scripts/hook_manifest.json +1 -1
  162. package/src/scripts/hook_manifest.yaml +14 -11
  163. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  164. package/src/scripts/hooks/category_a.ts +291 -0
  165. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  166. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  167. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  168. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  169. package/src/scripts/hooks/host_lowering.ts +215 -0
  170. package/src/scripts/hooks/host_lowering.yaml +185 -0
  171. package/src/scripts/hooks/host_semantics.ts +105 -37
  172. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  173. package/src/scripts/hooks/permission_gate.ts +117 -0
  174. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  175. package/src/scripts/hooks_status.ts +12 -10
  176. package/src/scripts/install.ts +57 -86
  177. package/src/scripts/install_anthropic_key.sh +2 -2
  178. package/src/scripts/lint_agent_security.ts +316 -25
  179. package/src/scripts/lint_confusables.ts +42 -6
  180. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  181. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  182. package/src/scripts/lint_handoffs.ts +0 -132
  183. package/src/scripts/lint_hidden_unicode.ts +49 -7
  184. package/src/scripts/lint_hook_manifest.ts +87 -3
  185. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  186. package/src/scripts/lint_mcp_config_security.ts +42 -6
  187. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  188. package/src/scripts/lint_readme_size.ts +158 -12
  189. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  190. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  191. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  192. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  193. package/src/scripts/mcp_server/resources.ts +41 -0
  194. package/src/scripts/mcp_server/telemetry.ts +19 -4
  195. package/src/scripts/measure_sibling_naming.ts +150 -0
  196. package/src/scripts/orchestration_record.ts +12 -1
  197. package/src/scripts/redact_hook_capture.ts +1 -1
  198. package/src/scripts/release.ts +24 -24
  199. package/src/scripts/release_publication.ts +6 -2
  200. package/src/scripts/report_skill_activation.ts +319 -7
  201. package/src/scripts/schemas/review-findings.schema.json +5 -0
  202. package/src/scripts/schemas/skill.schema.json +2 -2
  203. package/src/scripts/self_repair_cli.ts +31 -1
  204. package/src/scripts/session_canary_hook.ts +1 -1
  205. package/src/scripts/session_register_hook.ts +4 -3
  206. package/src/scripts/skill_scout.ts +69 -0
  207. package/src/scripts/trigger_eval_grandfather.json +5 -1
  208. package/src/scripts/validate_agent_settings.ts +1 -1
  209. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  210. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -109,7 +109,39 @@ export interface WorkerCapsule {
109
109
  * affordable precisely here — an envelope is consume-once and expires in
110
110
  * hours, so the migration window is a session, not a release.
111
111
  */
112
- export const CAPSULE_SCHEMA_VERSION = 3;
112
+ export const CAPSULE_SCHEMA_VERSION = 4;
113
+
114
+ /**
115
+ * Versions a reader ACCEPTS. Version 4 is the first bump that is purely
116
+ * ADDITIVE, and that is why it widens the accepted set instead of replacing it.
117
+ *
118
+ * The compatibility contract is stated here rather than inferred.
119
+ *
120
+ * - **Scope.** The version is ENVELOPE-WIDE, not per-variant. One number
121
+ * describes the whole schema module, so a variant added later does not
122
+ * silently re-date records of another variant.
123
+ * - **New reader, old record.** A v3 record validates. Its required set is v3's
124
+ * — the fields added at v4 are not demanded of it, because demanding them
125
+ * would be a retroactive requirement on records already written.
126
+ * - **Old reader, new record.** A v3-only reader rejects a v4 record with the
127
+ * version violation named. That is the pre-existing loud-discard behaviour and
128
+ * is affordable for the same reason v3 gave: a record is consume-once and
129
+ * expires in {@link RECYCLE_MAX_AGE_HOURS} hours, so the migration window is a
130
+ * session, not a release.
131
+ * - **Unknown version.** Anything outside this set is rejected with the set
132
+ * printed. Absent is not a pass — an undated record is not "probably current".
133
+ * - **Unknown variant.** Rejected by `variant` check, never coerced. A reader
134
+ * that does not know a variant must not guess at its required fields.
135
+ * - **Rollback.** Reverting to a v3-only reader after v4 records exist discards
136
+ * those records loudly rather than mis-reading them; the loss is one session's
137
+ * resume, which is the same loss as no record at all.
138
+ *
139
+ * Adding a required field is what forced v3 to be a hard break. v4 adds
140
+ * `successful_approaches`, `open_questions` and `predecessor`, and requires the
141
+ * first and third **only of records that declare themselves v4** — so no
142
+ * previously valid record becomes invalid.
143
+ */
144
+ export const ACCEPTED_CAPSULE_VERSIONS: readonly number[] = [3, 4];
113
145
 
114
146
  /** The two CHECKPOINT variants sharing this schema. */
115
147
  export const CAPSULE_VARIANTS = ['worker', 'main_session'] as const;
@@ -280,6 +312,30 @@ export function validateCapsule(input: unknown): string[] {
280
312
  * wire gate; this variant is consumed from a FILE by
281
313
  * `handoff_context_hook`, so the module itself must be the strict gate.
282
314
  */
315
+ /**
316
+ * The four questions a resuming session asks, and the field that answers each —
317
+ * written down rather than left implicit, because the gap that produced
318
+ * `successful_approaches` stayed invisible for exactly as long as the mapping
319
+ * was unwritten: three of the four had a field, the fourth had nothing, and no
320
+ * reader of the schema had a reason to notice.
321
+ *
322
+ * *What was it about?* is answered directly by `task`, with `summary` carrying
323
+ * the one-line outcome. *What was the goal?* is answered only by PROXY:
324
+ * `acceptance_criteria` states what **done** means, which is the goal expressed
325
+ * as a test rather than as an intention, and `next_task` narrows it to the
326
+ * successor's first move. *What did NOT work?* is `failed_approaches`, direct,
327
+ * required, explicit `none`. *What DID work?* is `successful_approaches`,
328
+ * direct, required at version 4, explicit `none`.
329
+ *
330
+ * The one proxy is labelled a proxy on purpose. A goal can be met by criteria
331
+ * nobody wrote down, so reading `acceptance_criteria` as the goal itself is the
332
+ * error the label exists to prevent.
333
+ *
334
+ * Everything else the schema carries — `remaining`, `not_carried_forward`,
335
+ * `constraints`, `decisions`, `open_questions`, the drift anchor — answers a
336
+ * question the four do not ask. That is not surplus; it is simply outside this
337
+ * mapping.
338
+ */
283
339
  export interface MainSessionRecycleEnvelope {
284
340
  /** Must be {@link CAPSULE_SCHEMA_VERSION}. */
285
341
  capsule_version: number;
@@ -371,6 +427,59 @@ export interface MainSessionRecycleEnvelope {
371
427
  * successor re-burning a recorded dead end.
372
428
  */
373
429
  failed_approaches: string[];
430
+ /**
431
+ * Approaches this session tried and KEPT — "did X, it worked because Y".
432
+ *
433
+ * **The counterpart `failed_approaches` did not have**, and its absence was
434
+ * a structural defect rather than an omission: a record that can say what to
435
+ * avoid and cannot say what to repeat is pessimistic by construction, and a
436
+ * successor re-derives every working decision from scratch while being
437
+ * warned off the dead ends.
438
+ *
439
+ * Required at capsule version 4 with at least one entry, for exactly the
440
+ * reason `failed_approaches` is: a session that kept nothing writes the
441
+ * single entry `none` explicitly, so "nothing worked" stays distinguishable
442
+ * from "nobody wrote it down". An optional field would re-create the silence
443
+ * one layer down.
444
+ */
445
+ successful_approaches?: string[];
446
+ /**
447
+ * Questions this session could not settle — the successor's first reading
448
+ * list. The worker variant has carried `open_risks` since v1
449
+ * (`WorkerCapsule.open_risks`) and the handoff template asks for open
450
+ * questions in prose; the session variant had neither, so the one thing a
451
+ * resuming session most needs to not silently drop had no field.
452
+ *
453
+ * Optional, unlike `successful_approaches`: an empty question list is not
454
+ * the same asymmetry — a record with nothing open is a complete claim, and
455
+ * `remaining` already carries the work.
456
+ */
457
+ open_questions?: string[];
458
+ /**
459
+ * The session that WROTE this record.
460
+ *
461
+ * Added with `predecessor` and not separable from it: an edge needs two
462
+ * endpoints, and before v4 a record carried neither. `workspace` answers
463
+ * "which checkout", which is what the identity check needs; it cannot
464
+ * answer "which session", which is what a chain needs.
465
+ */
466
+ session_id?: string;
467
+ /**
468
+ * The session this one continues, or the explicit string `none`.
469
+ *
470
+ * Required at capsule version 4. Before it, `grep -rn
471
+ * "predecessor_session\|lineage_id" src/` returned 0 and the reader
472
+ * injected whatever record was lying at the path — which is not the same
473
+ * question as "which record is mine" that the session key answers. A key
474
+ * says whose record this is; a predecessor edge says which record this
475
+ * session should be reading.
476
+ *
477
+ * `none` is a STATED ABSENCE, never an empty string or a missing field. The
478
+ * first session in a workspace has no predecessor and says so, so a reader
479
+ * never waits on something that will not arrive — and a NAMED predecessor
480
+ * that is absent is a refusal, never a fall-through to a different record.
481
+ */
482
+ predecessor?: string;
374
483
  /**
375
484
  * Drift anchor — all three, or none of them is one. Identity is the
376
485
  * canonicalized remote (or the realpath of the common git dir when there
@@ -615,6 +724,10 @@ const RECYCLE_ENVELOPE_KEYS: ReadonlySet<string> = new Set([
615
724
  'next_task',
616
725
  'suggested_skills',
617
726
  'failed_approaches',
727
+ 'successful_approaches',
728
+ 'open_questions',
729
+ 'predecessor',
730
+ 'session_id',
618
731
  'repo_identity',
619
732
  'branch',
620
733
  'head',
@@ -644,9 +757,16 @@ export function validateRecycleEnvelope(input: unknown): string[] {
644
757
  }
645
758
  }
646
759
 
647
- if (e['capsule_version'] !== CAPSULE_SCHEMA_VERSION) {
648
- errors.push(`capsule_version must be ${CAPSULE_SCHEMA_VERSION}`);
760
+ const version = e['capsule_version'];
761
+ if (typeof version !== 'number' || !ACCEPTED_CAPSULE_VERSIONS.includes(version)) {
762
+ errors.push(`capsule_version must be one of ${ACCEPTED_CAPSULE_VERSIONS.join(' | ')}`);
649
763
  }
764
+ // Requiredness is version-conditional, and only in the additive direction:
765
+ // a v4 record owes the v4 fields, a v3 record owes exactly what v3 owed.
766
+ // Demanding the new fields of an already-written record would be a
767
+ // retroactive requirement, which is the one thing the recorded schema lock
768
+ // forbids ("add fields or variants, never repurpose or remove one").
769
+ const atV4 = version === 4;
650
770
  if (e['variant'] !== 'main_session') {
651
771
  errors.push("variant must be 'main_session'");
652
772
  }
@@ -697,6 +817,38 @@ export function validateRecycleEnvelope(input: unknown): string[] {
697
817
  );
698
818
  }
699
819
 
820
+ // Version 4 additions. `successful_approaches` mirrors `failed_approaches`
821
+ // exactly — required with >= 1 entry, `none` written explicitly — because
822
+ // the asymmetry between the two is the defect the field exists to remove,
823
+ // and an optional counterpart to a required field re-creates it.
824
+ checkList(errors, 'successful_approaches', e['successful_approaches'], MAX_LINE_CHARS, atV4);
825
+ if (Array.isArray(e['successful_approaches']) && e['successful_approaches'].length === 0) {
826
+ errors.push(
827
+ 'successful_approaches must carry at least one entry — write "none" explicitly, so ' +
828
+ 'silence and "nothing worked" stay distinguishable',
829
+ );
830
+ }
831
+ checkList(errors, 'open_questions', e['open_questions'], MAX_LINE_CHARS, false);
832
+
833
+ // The predecessor edge (Phase 2.3). Required at v4, and an EMPTY value is
834
+ // not an answer: `none` is a claim ("this session starts a chain"), absence
835
+ // is a reader waiting for something that will not arrive.
836
+ const predecessor = e['predecessor'];
837
+ if (predecessor === undefined) {
838
+ if (atV4) {
839
+ errors.push(
840
+ 'predecessor is required — name the session this one continues, or write "none" ' +
841
+ 'explicitly for the first session in a workspace',
842
+ );
843
+ }
844
+ } else if (!isShortLine(predecessor, MAX_REF_CHARS)) {
845
+ errors.push(`predecessor must be a single line of 1–${MAX_REF_CHARS} chars ("none" when there is no predecessor)`);
846
+ }
847
+
848
+ if (e['session_id'] !== undefined && !isShortLine(e['session_id'], MAX_REF_CHARS)) {
849
+ errors.push(`session_id must be a single line of 1–${MAX_REF_CHARS} chars`);
850
+ }
851
+
700
852
  // Drift anchor (Phase 3.2). Optional as a set — an older envelope simply
701
853
  // has none — but each present field must still be a short line.
702
854
  for (const field of ['repo_identity', 'branch', 'head', 'status_summary', 'last_verify'] as const) {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * surface — the IDE / CLI / cloud dimension of a dispatch, as an ENVELOPE
3
+ * field rather than a platform key.
4
+ *
5
+ * WHY A FIELD AND NOT A PLATFORM. `_lib/session_register.ts` records that the
6
+ * capability lattice has no IDE/CLI dimension, which is why it reports cursor
7
+ * covered whether or not its per-turn slots fire there. Splitting `cursor`
8
+ * into `cursor-ide` / `cursor-cli` would change `--platform`, the eight
9
+ * platform identifiers, and every manifest row keyed on them. A field changes
10
+ * none of those and is still the thing `host_lowering.yaml`'s `(host, surface,
11
+ * slot)` key needs.
12
+ *
13
+ * WHAT THIS CAN AND CANNOT DECIDE — the honest half.
14
+ *
15
+ * `unknown` is the default and, today, very nearly the only answer. Nothing in
16
+ * this tree distinguishes an IDE session from a CLI one on any host: no host
17
+ * payload observed here carries such a marker, and inferring it from a TTY or
18
+ * a terminal name would be a guess wearing a function's authority — the exact
19
+ * move the surrounding roadmap exists to remove. So there is no `ide` or `cli`
20
+ * branch. When a host is observed to send a distinguishing marker, it is added
21
+ * to {@link CLOUD_PAYLOAD_MARKERS}'s sibling with a dated citation, and the
22
+ * branch arrives with the evidence rather than ahead of it.
23
+ *
24
+ * The `cloud` branch is a DECLARED VOCABULARY, not an observation. This package
25
+ * will honour these payload keys if a host sends one; none has been observed
26
+ * sending one. That distinction is the point: the mechanism is real and tested,
27
+ * the host claim is absent and says so.
28
+ */
29
+
30
+ /** The closed surface vocabulary. `unknown` is the safe default. */
31
+ export const SURFACES = ['ide', 'cli', 'cloud', 'unknown'] as const;
32
+ export type Surface = (typeof SURFACES)[number];
33
+
34
+ /**
35
+ * Payload keys whose truthy presence declares a background / cloud agent.
36
+ *
37
+ * Declared, not observed — see the module header. Kept as a list so a host that
38
+ * starts sending one needs no code change, only a citation in this comment.
39
+ */
40
+ export const CLOUD_PAYLOAD_MARKERS: readonly string[] = [
41
+ 'background_agent',
42
+ 'is_background',
43
+ 'isBackgroundAgent',
44
+ ];
45
+
46
+ /** Explicit override, for a caller that already knows the surface. */
47
+ export const SURFACE_ENV_VAR = 'AGENT_CONFIG_SURFACE';
48
+
49
+ function _isSurface(v: unknown): v is Surface {
50
+ return typeof v === 'string' && (SURFACES as readonly string[]).includes(v);
51
+ }
52
+
53
+ /**
54
+ * Resolve the surface for one dispatch.
55
+ *
56
+ * Precedence: an explicit environment override, then a declared payload
57
+ * marker, then `unknown`. An unrecognised override value is ignored rather
58
+ * than trusted — a typo must not become a capability claim.
59
+ */
60
+ export function detectSurface(
61
+ payload: unknown,
62
+ env: Record<string, string | undefined> = process.env,
63
+ ): Surface {
64
+ const override = env[SURFACE_ENV_VAR];
65
+ if (_isSurface(override)) return override;
66
+
67
+ if (typeof payload === 'object' && payload !== null && !Array.isArray(payload)) {
68
+ const obj = payload as Record<string, unknown>;
69
+ for (const key of CLOUD_PAYLOAD_MARKERS) {
70
+ if (obj[key]) return 'cloud';
71
+ }
72
+ }
73
+ return 'unknown';
74
+ }
75
+
76
+ /**
77
+ * Read a persisted or caller-supplied surface value.
78
+ *
79
+ * Tolerant, not a cast: an unrecognised value — a row written by a newer build,
80
+ * a typo, an absent field — reads as `unknown`. Same posture as
81
+ * `readRunTerminalState`, and for the same reason: a value this build cannot
82
+ * place must not fail a write or a read.
83
+ */
84
+ export function readSurface(v: unknown): Surface {
85
+ return _isSurface(v) ? v : 'unknown';
86
+ }
@@ -341,3 +341,119 @@ export function operationConflicts(authority: UiAuthority, op: Operation): Confl
341
341
  }
342
342
  return out;
343
343
  }
344
+
345
+ /* -------------------------------------------------------------------------- */
346
+ /* Antipattern signal classes against the precedence list */
347
+ /* -------------------------------------------------------------------------- */
348
+
349
+ /**
350
+ * The five classes a `design-antipatterns.md` catalog row may claim.
351
+ *
352
+ * Kept here, beside the precedence list, rather than in the catalog gate: the
353
+ * question "may this signal overrule what the user supplied" is a precedence
354
+ * question, and precedence lives in exactly one module by construction.
355
+ */
356
+ export type SignalClass =
357
+ | 'floor'
358
+ | 'invariant'
359
+ | 'craft-presumption'
360
+ | 'style-preference'
361
+ | 'reference-constraint';
362
+
363
+ /** The two classes that are opinions rather than thresholds. */
364
+ export const SOFT_CLASSES = ['craft-presumption', 'style-preference'] as const;
365
+
366
+ export interface SignalVerdict {
367
+ /**
368
+ * `blocks` — refuse the surface. `flags` — surface it as a rebuttable
369
+ * presumption. `yields` — say nothing: something with more authority has
370
+ * already decided this.
371
+ */
372
+ outcome: 'blocks' | 'flags' | 'yields';
373
+ /** The precedence level that outranked it, numbered as in this file's header. */
374
+ outranked_by: { level: number; source: ProvenanceSource; detail: string } | null;
375
+ reason: string;
376
+ }
377
+
378
+ /**
379
+ * Where a soft signal sits against the precedence list.
380
+ *
381
+ * A craft presumption is a good default and nothing more. When the run has a
382
+ * supplied reference (4), a coherent incumbent (5), or a project `DESIGN.md`
383
+ * register (6), that artifact has already decided the question the presumption
384
+ * would re-open, and re-opening it is the redesign-by-taste failure
385
+ * `design-fidelity` exists to stop.
386
+ *
387
+ * A `floor` or an `invariant` is NOT a taste call — an accessibility threshold
388
+ * or a structural correctness rule — so nothing on the list releases it, and a
389
+ * reference artifact that violates one is a finding about the reference.
390
+ *
391
+ * LEVELS 1–3 ARE DELIBERATELY NOT MECHANISED HERE. Explicit user authority, a
392
+ * registered hard constraint, and the surface brief all outrank a presumption a
393
+ * fortiori, but `Signals` carries them only as whole-surface fields
394
+ * (`surface_mode`, `register`, `change_intent`) with no per-pattern statement.
395
+ * Reading a brief's mere presence as blanket permission for every antipattern
396
+ * would silence the catalog on every briefed surface, which is the opposite of
397
+ * what those levels mean. A per-pattern override is `DESIGN.md`'s declared
398
+ * suppression, which is level 6 and IS handled.
399
+ */
400
+ export function resolveSignal(signalClass: SignalClass, signals: Signals = {}): SignalVerdict {
401
+ if (signalClass === 'floor' || signalClass === 'invariant') {
402
+ return {
403
+ outcome: 'blocks',
404
+ outranked_by: null,
405
+ reason:
406
+ `a ${signalClass} is a threshold, not a preference — no supplied artifact releases it, `
407
+ + 'and an artifact that violates one is a finding about the artifact',
408
+ };
409
+ }
410
+
411
+ if (signalClass === 'reference-constraint') {
412
+ const present = signals.reference !== undefined || signals.design_md?.present === true;
413
+ return present
414
+ ? {
415
+ outcome: 'blocks',
416
+ outranked_by: null,
417
+ reason: 'the reference or DESIGN.md this constraint is bound to is present in this run, so it carries that artifact\'s own authority',
418
+ }
419
+ : {
420
+ outcome: 'flags',
421
+ outranked_by: null,
422
+ reason: 'the reference this constraint is bound to is not present in this run, so it has no artifact behind it',
423
+ };
424
+ }
425
+
426
+ if (signals.reference !== undefined) {
427
+ return {
428
+ outcome: 'yields',
429
+ outranked_by: { level: 4, source: 'reference-artifact', detail: signals.reference.path ?? signals.reference.maturity },
430
+ reason: `a ${signalClass} never outranks a supplied reference artifact`,
431
+ };
432
+ }
433
+ if (signals.incumbent?.coherent === true) {
434
+ return {
435
+ outcome: 'yields',
436
+ outranked_by: { level: 5, source: 'incumbent-scan', detail: 'coherent incumbent' },
437
+ reason: `a ${signalClass} never outranks a coherent incumbent`,
438
+ };
439
+ }
440
+ if (signals.design_md?.present === true) {
441
+ return {
442
+ outcome: 'yields',
443
+ outranked_by: { level: 6, source: 'design-md', detail: signals.design_md.register ?? 'register unstated' },
444
+ reason: `a ${signalClass} never outranks a project DESIGN.md register`,
445
+ };
446
+ }
447
+ if (signals.product_md?.present === true) {
448
+ return {
449
+ outcome: 'yields',
450
+ outranked_by: { level: 6, source: 'product-md', detail: signals.product_md.register ?? 'register unstated' },
451
+ reason: `a ${signalClass} never outranks a project PRODUCT.md register`,
452
+ };
453
+ }
454
+ return {
455
+ outcome: 'flags',
456
+ outranked_by: null,
457
+ reason: `a ${signalClass} with nothing above it on the precedence list is a rebuttable presumption`,
458
+ };
459
+ }
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import process from 'node:process';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
- import { readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
19
+ import { dated_trigger_day, readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
20
20
  import { generatedByItalic } from '../_lib/generated_by.js';
21
21
 
22
22
  // ^ADR-(\d{3})-([a-z0-9-]+)\.md$
@@ -262,6 +262,41 @@ export function scanOutside(repoRoot: string, roots: readonly string[] = OUTSIDE
262
262
  return out;
263
263
  }
264
264
 
265
+ /**
266
+ * The rows whose `review_trigger` opens with a calendar date.
267
+ *
268
+ * WHY THIS SECTION CARRIES THE DATE AND NOT THE FIRED/NOT-FIRED STATE
269
+ * ------------------------------------------------------------------
270
+ * `regenerate_index --check` byte-compares this file in CI
271
+ * (`.github/workflows/rule-backstops.yml`), so anything rendered here has to be
272
+ * a function of the tree alone. A `fired` / `not-fired` column would be a
273
+ * function of the tree AND the day, which means the index would go stale on a
274
+ * calendar boundary nobody committed and red every open pull request until
275
+ * someone regenerated it. The date is the decidable half; the reader compares
276
+ * it to today, and `adr_cite_check` prints the comparison for a machine.
277
+ */
278
+ export interface DatedTriggerRow {
279
+ label: string;
280
+ path: string;
281
+ day: string;
282
+ decision: string;
283
+ }
284
+
285
+ export function datedTriggerRows(rows: readonly Row[]): DatedTriggerRow[] {
286
+ const out: DatedTriggerRow[] = [];
287
+ for (const r of rows) {
288
+ const day = dated_trigger_day(r.review_trigger ?? '');
289
+ if (day === null) continue;
290
+ out.push({
291
+ label: 'num' in r ? `ADR-${r.num as string}` : (r.path as string).slice(0, -3),
292
+ path: r.path as string,
293
+ day,
294
+ decision: r.decision ?? r.slug ?? '—',
295
+ });
296
+ }
297
+ return out.sort((a, b) => a.day.localeCompare(b.day) || a.label.localeCompare(b.label));
298
+ }
299
+
265
300
  /** Mirror `render(num, leg)`. */
266
301
  export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): string {
267
302
  const out = [
@@ -277,6 +312,24 @@ export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): stri
277
312
  if (leg.length > 0) {
278
313
  out.push('', '## Unnumbered (legacy)', '', HEAD, ...leg.map(row));
279
314
  }
315
+ const dated = datedTriggerRows([...num, ...leg]);
316
+ if (dated.length > 0) {
317
+ out.push(
318
+ '',
319
+ '## Dated review triggers',
320
+ '',
321
+ 'A `review_trigger` that opens with a calendar date is the one reopen condition',
322
+ 'this repository can decide without interpreting anything, so it is listed where a',
323
+ 'reader meets it rather than only in a command they would have to know to run.',
324
+ 'Compare the expiry to today; `./scripts-run src/scripts/adr_cite_check <ADR-NNN>`',
325
+ 'prints `fired` or `not-fired` for the same date. Every other trigger is semantic',
326
+ 'and reports `indeterminate` — a trigger that merely mentions a date is not listed.',
327
+ '',
328
+ '| Expiry | # | Decision |',
329
+ '|---|---|---|',
330
+ ...dated.map((d) => `| ${d.day} | [${d.label}](${d.path}) | ${d.decision} |`),
331
+ );
332
+ }
280
333
  if (outside.length > 0) {
281
334
  out.push(
282
335
  '',
@@ -25,7 +25,7 @@
25
25
  * (measured: ADR-035 still asserts a rejection ADR-232 reopened).
26
26
  * - **review_trigger** — verbatim, plus a state.
27
27
  *
28
- * `indeterminate` is a FIRST-CLASS trigger result, not a failure. Every
28
+ * `indeterminate` is a FIRST-CLASS trigger result, not a failure. Almost every
29
29
  * `review_trigger` in the corpus is a semantic condition ("reopen when the
30
30
  * capacity premise changes", "if a fifth band appears"), so forcing them to a
31
31
  * boolean would convert uncertainty into either permission or blockage. Both
@@ -33,6 +33,29 @@
33
33
  * trigger grammar as a prerequisite for exactly this reason. An `indeterminate`
34
34
  * result means: this may not be presented as an unqualified lock — route it.
35
35
  *
36
+ * THE DATED SUB-CLASS, AND WHY THE 2026-08-19 REJECTION DOES NOT REACH IT
37
+ * ----------------------------------------------------------------------
38
+ * The sentence above used to open "Every `review_trigger` in the corpus is a
39
+ * semantic condition"; it is weakened rather than deleted because the claim was
40
+ * true when written and one record has since falsified it.
41
+ *
42
+ * What those two seats rejected was forcing SEMANTIC conditions to a boolean.
43
+ * A trigger that OPENS with a calendar date is not one: `fired` / `not-fired`
44
+ * is decided by comparing that date to `asOf()`, which is arithmetic and
45
+ * carries no interpretation of the tree, so a second reader of the same commit
46
+ * gets the same answer. That is the entire carve-out — a leading
47
+ * `Expiry YYYY-MM-DD`, or a bare leading ISO date, and nothing else. A
48
+ * condition that merely MENTIONS a date somewhere in its prose ("revisit at the
49
+ * 2026-11-10 review date, or earlier on either observable event") is a semantic
50
+ * condition and stays `indeterminate`, which is what keeps this from being the
51
+ * rejected grammar by increments.
52
+ *
53
+ * `dated-unparsed` exists so the carve-out cannot fail silently. A trigger that
54
+ * announces itself as dated and whose date this parser cannot read is a defect
55
+ * in the record or in the parser; returning `indeterminate` for it would hide
56
+ * that defect behind the exact state every trigger had before the carve-out
57
+ * existed, where no test could ever see it as a regression.
58
+ *
36
59
  * It also prints the two descriptive axes (`provenance`, `evidence`) and
37
60
  * `authority_basis`, and for an accepted E0/E1 record that does NOT carry
38
61
  * `authority_basis: owner_intent` it prints
@@ -73,12 +96,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
73
96
  import {
74
97
  type AdrFrontmatter,
75
98
  authorityBasisOf,
99
+ dated_trigger_day,
100
+ DATED_TRIGGER_PROBE,
101
+ iso_day,
76
102
  evidenceOf,
77
103
  isLowEvidenceAccepted,
78
104
  provenanceOf,
79
105
  readAdrFrontmatter,
80
106
  readAdrFrontmatterScalars,
81
107
  } from './_lib/adr_frontmatter.js';
108
+ import { asOf } from './_lib/as_of.js';
82
109
  import { GateLedger } from './_lib/gate_ledger.js';
83
110
  import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
84
111
  import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
@@ -149,8 +176,12 @@ export const SURFACES_NOT_SCANNED = [
149
176
  */
150
177
  export const PARTIAL_COVERAGE: readonly string[] = [];
151
178
 
152
- /** A trigger state. `none` means the ADR never recorded a reopen condition. */
153
- export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired';
179
+ /**
180
+ * A trigger state. `none` means the ADR never recorded a reopen condition;
181
+ * `dated-unparsed` means it recorded one that announces a date this parser
182
+ * could not read, which is a defect and not an answer.
183
+ */
184
+ export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired' | 'dated-unparsed';
154
185
 
155
186
  export interface CiteResult {
156
187
  ref: string;
@@ -469,12 +500,21 @@ export function amendment_blocks(body: string): string[] {
469
500
  /**
470
501
  * A trigger's state, from what is mechanically decidable and nothing else.
471
502
  *
472
- * There is no clause that returns `fired` or `not-fired` from prose, and that
473
- * absence is the design: every trigger in the corpus is semantic. The states
474
- * exist in the type because a future structured trigger can carry them; today
475
- * the honest answers are `none` and `indeterminate`.
503
+ * Two clauses return a decided answer, and both read a leading calendar date
504
+ * and nothing else — see `DATED_TRIGGER_RE` for why the anchor is the safety
505
+ * property. Every semantic trigger still returns `indeterminate`, which is a
506
+ * first-class result here and not a failure.
507
+ *
508
+ * The clock is read ONLY when a dated trigger is actually present — `asOf()`
509
+ * warns on stderr for an unpinned run, and a corpus with no dated trigger has
510
+ * no verdict that depends on the hour, so announcing one there would be noise
511
+ * on a stream other tools read beside the payload.
512
+ *
513
+ * @param asOfDate the "now" to compare against; defaults to the `asOf()` seam,
514
+ * so a CI run compares against the commit date and is
515
+ * reproducible from the commit alone.
476
516
  */
477
- export function trigger_state(fm: Record<string, string>): TriggerState {
517
+ export function trigger_state(fm: Record<string, string>, asOfDate?: Date): TriggerState {
478
518
  const t = (fm['review_trigger'] ?? '').trim();
479
519
  if (t === '') return 'none';
480
520
  // The transitional migration value records that no condition has been
@@ -485,6 +525,9 @@ export function trigger_state(fm: Record<string, string>): TriggerState {
485
525
  // nobody has evaluated. Found in completion review; the field did not
486
526
  // exist in this file's corpus when the mapping was written.
487
527
  if (t.toLowerCase() === 'unclassified') return 'none';
528
+ const day = dated_trigger_day(t);
529
+ if (day !== null) return iso_day(asOfDate ?? asOf()) >= day ? 'fired' : 'not-fired';
530
+ if (DATED_TRIGGER_PROBE.test(t)) return 'dated-unparsed';
488
531
  return 'indeterminate';
489
532
  }
490
533
 
@@ -530,6 +573,15 @@ export function verdict_for(r: Omit<CiteResult, 'verdict'>): string {
530
573
  if (r.trigger_state === 'none') {
531
574
  return 'LIVE, NO REOPEN CONDITION — the ADR records no `review_trigger`, so nothing would ever reopen it on its own. Treat that as a defect in the ADR, not as strength of the lock.';
532
575
  }
576
+ if (r.trigger_state === 'fired') {
577
+ return 'LIVE, TRIGGER FIRED — the reopen condition is a date and that date has passed. The record still binds until someone acts on it, and citing it without naming the fired trigger presents a lapsed review as a current one.';
578
+ }
579
+ if (r.trigger_state === 'not-fired') {
580
+ return 'LIVE, TRIGGER NOT YET FIRED — the reopen condition is a date still in the future. This is the one state in which a trigger genuinely adds nothing to the citation, and it stops being true on a known day.';
581
+ }
582
+ if (r.trigger_state === 'dated-unparsed') {
583
+ return 'LIVE, DATED TRIGGER UNREADABLE — the reopen condition announces a date this tool could not parse. That is a defect in the record or in the parser, not a verdict: fix the trigger to open with `Expiry YYYY-MM-DD` rather than reading this as "unknown".';
584
+ }
533
585
  return 'LIVE, TRIGGER INDETERMINATE — the reopen condition is semantic and this tool cannot decide it. Not an unqualified lock: evaluate the condition against the current tree and route the result.';
534
586
  }
535
587
 
@@ -823,21 +875,25 @@ export interface CorpusRow {
823
875
  /** Does the record carry a `review_trigger` at all? */
824
876
  has_trigger: boolean;
825
877
  /** Only meaningful when `has_trigger`; see `TRIGGER_VERDICTS`. */
826
- trigger: 'fired' | 'not-fired' | 'indeterminate' | 'none';
878
+ trigger: TriggerState;
827
879
  reopen_policy: string;
828
880
  /** Cited anywhere OUTSIDE `docs/decisions/`. */
829
881
  cited_outside: boolean;
830
882
  }
831
883
 
832
884
  /**
833
- * The three states 2.2 requires, and why a fourth exists beside them.
885
+ * The states a record CARRYING a trigger can be in, and why `none` is not one.
886
+ *
887
+ * `none` is outside the denominator, not a bucket inside it: the survey asks
888
+ * that these sum to "the number of ADRs carrying a trigger", and a record with
889
+ * no trigger carries none.
834
890
  *
835
- * `none` is NOT one of the three. The step asks that fired + not-fired +
836
- * indeterminate sum to "the number of ADRs carrying a trigger", so a record
837
- * with no trigger is outside the denominator rather than a fourth bucket
838
- * inside it.
891
+ * `dated-unparsed` was added with the dated sub-class and belongs in the sum
892
+ * for the same reason `none` does not — a record in that state DOES carry a
893
+ * trigger. Leaving it out would have made the sum line report a mismatch, i.e.
894
+ * announced a defect in the survey where the defect is in the ADR.
839
895
  */
840
- export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate'] as const;
896
+ export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate', 'dated-unparsed'] as const;
841
897
 
842
898
  /** Every ADR file, surveyed. */
843
899
  export function corpus_survey(repo_root: string = REPO_ROOT): CorpusRow[] {
@@ -1,7 +1,7 @@
1
1
  // External-AI clients for the council — ported from the retired Python
2
2
  // `src/scripts/ai_council/clients.py` (ADR-200).
3
3
  //
4
- // Mirrors the contract from `scripts/skill_trigger_eval.py`:
4
+ // Mirrors the contract from `src/scripts/skill_trigger_eval.ts`:
5
5
  // - Tokens come exclusively from `~/.event4u/agent-config/<provider>.key`
6
6
  // (legacy `~/.config/agent-config/<provider>.key` is read as a fallback so
7
7
  // pre-2.4 installs keep working until the user moves the files into the new