@event4u/agent-config 14.5.0 → 14.7.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 (203) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +384 -3
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/decision.md +7 -0
  5. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  6. package/dist/agent-src/commands/analyze.md +1 -1
  7. package/dist/agent-src/commands/optimize/project.md +10 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +132 -4
  9. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  10. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
  11. package/dist/agent-src/rules/context-hygiene.md +2 -0
  12. package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
  13. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  14. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  15. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  16. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  17. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  18. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  19. package/dist/agent-src/rules/skill-quality.md +0 -2
  20. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  21. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  22. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  23. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  24. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  25. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  27. package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
  28. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  29. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  30. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  31. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  32. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  33. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  34. package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
  35. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  36. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  37. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  38. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  39. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  40. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  41. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  42. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  43. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  44. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  45. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  46. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  47. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  48. package/dist/agent-src/templates/roadmaps.md +45 -2
  49. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  50. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  51. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  52. package/dist/cli/registry.js +1 -1
  53. package/dist/cli/registry.js.map +1 -1
  54. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  55. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  56. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  57. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  58. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  59. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  60. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  61. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  62. package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
  63. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  64. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  65. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  66. package/dist/cli-delegate/cmd_conformance.js +459 -709
  67. package/dist/cli-delegate/cmd_doctor.js +6 -6
  68. package/dist/cli-delegate/cmd_explain.js +1 -1
  69. package/dist/cli-delegate/cmd_export.js +7 -6
  70. package/dist/cli-delegate/cmd_handoff.js +6 -6
  71. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  72. package/dist/cli-delegate/cmd_preflight.js +2 -2
  73. package/dist/cli-delegate/cmd_prune.js +7 -6
  74. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  75. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  76. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  77. package/dist/cli-delegate/cmd_sync.js +7 -6
  78. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  79. package/dist/cli-delegate/cmd_update.js +1 -1
  80. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  81. package/dist/cli-delegate/cmd_validate.js +7 -6
  82. package/dist/discovery/deprecation-report.md +1 -1
  83. package/dist/discovery/discovery-manifest.json +72 -29
  84. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  85. package/dist/discovery/discovery-manifest.summary.md +4 -4
  86. package/dist/discovery/dormancy-report.md +1 -1
  87. package/dist/discovery/orphan-report.md +1 -1
  88. package/dist/discovery/packs.json +10 -8
  89. package/dist/discovery/trust-report.md +3 -3
  90. package/dist/discovery/workspaces.json +8 -6
  91. package/dist/hooks/dispatch.js +3127 -2022
  92. package/dist/install/claudePathsPlan.js +187 -0
  93. package/dist/install/claudePathsPlan.js.map +1 -0
  94. package/dist/install/hostLayerFingerprint.js +136 -0
  95. package/dist/install/hostLayerFingerprint.js.map +1 -0
  96. package/dist/install/install.mjs +553 -425
  97. package/dist/install/partitionEligibility.js +276 -0
  98. package/dist/install/partitionEligibility.js.map +1 -0
  99. package/dist/mcp/registry-manifest.json +3 -3
  100. package/dist/mcp/server.json +2 -2
  101. package/dist/mcp/server.mjs +1744 -1191
  102. package/dist/router.json +1 -1
  103. package/dist/scripts/_lib/agent_src.js +41 -0
  104. package/dist/scripts/_lib/agent_src.js.map +1 -1
  105. package/dist/scripts/_lib/install_layout.js +83 -0
  106. package/dist/scripts/_lib/install_layout.js.map +1 -0
  107. package/dist/scripts/_lib/installed_lock.js +444 -0
  108. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  109. package/dist/scripts/condense.js +13 -164
  110. package/dist/scripts/condense.js.map +1 -1
  111. package/dist/server/schemas/settings.js +4 -0
  112. package/dist/server/schemas/settings.js.map +1 -1
  113. package/docs/contracts/settings-classes.md +12 -6
  114. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  115. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  116. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
  117. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  118. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  119. package/package.json +1 -1
  120. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  121. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  122. package/src/agent-src/scripts/gate_budget.ts +282 -0
  123. package/src/agent-src/scripts/gate_execute.ts +72 -31
  124. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  125. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  126. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  127. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  128. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  129. package/src/config/agent-settings.template.yml +27 -2
  130. package/src/config/ci-local-parity.yml +18 -0
  131. package/src/config/estate-count-budget.json +367 -3
  132. package/src/config/gate-violation-baselines.json +41 -41
  133. package/src/config/pack-size-budget.json +3 -2
  134. package/src/config/rule-activation-census.json +14 -6
  135. package/src/config/rule-stub-ceilings.json +19 -6
  136. package/src/install/claudePathsPlan.ts +196 -0
  137. package/src/install/hostLayerFingerprint.ts +145 -0
  138. package/src/install/partitionEligibility.ts +325 -0
  139. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  140. package/src/scripts/_dispatch.bash +6 -1
  141. package/src/scripts/_lib/ac_heading.ts +30 -0
  142. package/src/scripts/_lib/agent_src.ts +42 -0
  143. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  144. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  145. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  146. package/src/scripts/_lib/installed_lock.ts +50 -5
  147. package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
  148. package/src/scripts/_lib/md_section.ts +129 -0
  149. package/src/scripts/_lib/scope_guard.sh +86 -4
  150. package/src/scripts/_lib/self_repair.ts +19 -0
  151. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +81 -1
  153. package/src/scripts/adr/regenerate_index.ts +13 -2
  154. package/src/scripts/adr_cite_check.ts +424 -0
  155. package/src/scripts/ai_council/cli_help.ts +55 -0
  156. package/src/scripts/before_complete_hook.ts +153 -28
  157. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  158. package/src/scripts/check_adr_frontmatter.ts +187 -1
  159. package/src/scripts/check_references.ts +60 -0
  160. package/src/scripts/check_release_published.ts +32 -11
  161. package/src/scripts/check_single_delivery.ts +517 -0
  162. package/src/scripts/ci_settle.ts +170 -0
  163. package/src/scripts/condense.ts +20 -170
  164. package/src/scripts/council_cli.ts +3 -3
  165. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  166. package/src/scripts/hook_manifest.json +1 -1
  167. package/src/scripts/hook_manifest.yaml +77 -14
  168. package/src/scripts/hooks/block_no_verify.ts +61 -10
  169. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  170. package/src/scripts/hooks/concern_registry.ts +4 -0
  171. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  172. package/src/scripts/hooks/run_continuation_hook.ts +887 -68
  173. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  174. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  175. package/src/scripts/hooks/state_io.ts +782 -20
  176. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  178. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  179. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  181. package/src/scripts/install.sh +12 -2
  182. package/src/scripts/install.ts +16 -11
  183. package/src/scripts/language_mirror_hook.ts +561 -25
  184. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  185. package/src/scripts/lint_plan_risk_register.ts +4 -3
  186. package/src/scripts/lint_skill_router_head.ts +4 -4
  187. package/src/scripts/mcp_server/tools.ts +2 -2
  188. package/src/scripts/memory_consolidation.ts +189 -0
  189. package/src/scripts/memory_eviction.ts +384 -0
  190. package/src/scripts/new_roadmap.ts +136 -0
  191. package/src/scripts/pack_mcp_content.ts +20 -5
  192. package/src/scripts/release.ts +37 -38
  193. package/src/scripts/report_layer_overlap.ts +78 -0
  194. package/src/scripts/report_memory_pointers.ts +622 -0
  195. package/src/scripts/self_repair_hook.ts +37 -5
  196. package/src/scripts/session_register_hook.ts +115 -15
  197. package/src/scripts/skill_usage_collect.ts +11 -1
  198. package/src/scripts/skill_usage_report.ts +278 -8
  199. package/src/scripts/sync_pr_branch.ts +7 -0
  200. package/src/server/schemas/settings.ts +8 -0
  201. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  202. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  203. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -0,0 +1,622 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Pointer-integrity report over the curated engineering memory store.
4
+ *
5
+ * WHY THIS EXISTS (road-to-context-fidelity Phase 2, spike cf02).
6
+ * `memory_report` prints `staleness-rate=0.0%` over a store measured by hand at
7
+ * **21.5 %** the same day. The 0.0 % is an artefact: all 107 entries carry the
8
+ * same `last_validated` and the same `review_after_days: 365`, so the earliest
9
+ * date any entry can read stale is 2027-07-09. The age axis is a check that
10
+ * passes because it cannot fail.
11
+ *
12
+ * WHAT THIS IS NOT, AND THE MEASUREMENT THAT SETTLED IT.
13
+ * This shipped as a staleness TRIAGE ranker — the council's Option 2, unanimous
14
+ * 2/2 on 2026-08-19 — on the argument that pointer liveness would rank entries
15
+ * for human re-verification. That claim was then measured against cf02's
16
+ * hand-walked ground truth and **failed**: precision 0.0 % against a 20.6 %
17
+ * base rate, recall 0.0 %, lift **0.00x** on the flagged set and 0.35x on the
18
+ * dead-or-moved union. Worse than random, on the very corpus that motivated it.
19
+ * The council's own pre-registered falsifier fired immediately. Full numbers and
20
+ * reproduction: `agents/evidence/eval-findings/context-fidelity-cf04.md`.
21
+ *
22
+ * The mechanism is not mysterious. A curated entry's `body` is free prose, so
23
+ * "does the tree still support this claim" is a SEMANTIC judgement no script
24
+ * decides. What pointers measure is whether the entry's CITATIONS still resolve
25
+ * — a different failure, and empirically an uncorrelated one. cf02 already
26
+ * carried the counterexample: `typecheck-use-task-not-bare-tsc` cites live
27
+ * files throughout and its stated reason is false.
28
+ *
29
+ * So the ranking claim is withdrawn and the name follows it. What survived
30
+ * measurement, and all this report asserts, is pointer INTEGRITY:
31
+ * - `dead` — a repo-rooted citation that resolves to nothing.
32
+ * - `moved` — the file exists under a new path; re-anchor the citation.
33
+ * - `unparseable` — outside the grammar or gitignored; parser-coverage only.
34
+ * - anchor coverage — how many entries can be tied to a tree state at all.
35
+ * Those are real documentation debt and a real prerequisite for the ladder.
36
+ * None of them is evidence that an entry's claim is false.
37
+ *
38
+ * It NEVER writes to the store and NEVER demotes an entry. Demotion is
39
+ * `memory_eviction.ts`, and the signal it acts on is a recorded HUMAN verdict.
40
+ * Council 2026-08-19: "Automatic demotion: never from pointer-liveness or
41
+ * anchor-drift output alone." The measurement above turned that from a
42
+ * precaution into a finding.
43
+ *
44
+ * Not a gate: it always exits 0 and has no gate-coverage entry. A blocking gate
45
+ * over a signal with 0.00x lift would be a merge blocker made of noise.
46
+ *
47
+ * Usage:
48
+ * report_memory_pointers # text report
49
+ * report_memory_pointers --format json
50
+ * report_memory_pointers --limit 10 # worst 10 entries only
51
+ * report_memory_pointers --path agents/memory
52
+ *
53
+ * Exit codes: 0 always (report), 3 = internal error.
54
+ */
55
+
56
+ import { spawnSync } from 'node:child_process';
57
+ import * as fs from 'node:fs';
58
+ import * as path from 'node:path';
59
+ import process from 'node:process';
60
+ import { fileURLToPath } from 'node:url';
61
+ import YAML from 'yaml';
62
+
63
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
64
+
65
+ const PROG = 'report_memory_pointers.ts';
66
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
67
+ const DEFAULT_STORE = 'agents/memory';
68
+
69
+ /** File extensions a bare path reference may carry. Bounded on purpose. */
70
+ const KNOWN_EXTENSIONS = new Set([
71
+ '.ts', '.js', '.mjs', '.cjs', '.md', '.yml', '.yaml', '.json', '.sh', '.py', '.txt',
72
+ ]);
73
+
74
+ /**
75
+ * Pointer classes the grammar recognises. Anything backticked that looks
76
+ * path-ish but matches no class is reported `unparseable` rather than
77
+ * silently treated as live — council refinement 2026-08-19: "ambiguous
78
+ * free-prose references should be reported as unparseable, not silently
79
+ * treated as live."
80
+ */
81
+ export type PointerKind = 'path' | 'path-line' | 'dir' | 'adr' | 'wiki-link' | 'unparseable';
82
+ /**
83
+ * `moved` is the state that made this instrument usable at all. The first
84
+ * measured run flagged 18 entries of which 16 cited a roadmap that had simply
85
+ * been archived — the file exists, at a new path, and the entry's CLAIM is
86
+ * untouched by the move. Counting those as dead put the sweep's precision
87
+ * (2/18 = 11 %) BELOW the store's own staleness base rate (21.5 %), i.e. the
88
+ * ranking was worse than random. Relocation-awareness is what separates
89
+ * "this pointer rotted" from "this file moved".
90
+ */
91
+ export type PointerState = 'live' | 'moved' | 'dead' | 'unparseable';
92
+
93
+ export interface Pointer {
94
+ raw: string;
95
+ kind: PointerKind;
96
+ state: PointerState;
97
+ /** Repo-relative path this pointer resolves to, when it resolves at all. */
98
+ target?: string;
99
+ /** Why a `dead` pointer is dead — the half a reader acts on. */
100
+ detail?: string;
101
+ }
102
+
103
+ export interface EntryReport {
104
+ id: string;
105
+ type: string;
106
+ status: string;
107
+ last_validated: string;
108
+ review_after_days: number | null;
109
+ /** The tree revision the entry was SEMANTICALLY verified against. */
110
+ verified_at_commit: string | null;
111
+ anchor_state: 'present' | 'missing' | 'unresolvable';
112
+ pointers: Pointer[];
113
+ dead: number;
114
+ moved: number;
115
+ live: number;
116
+ unparseable: number;
117
+ /** Cited paths that changed between the anchor and HEAD. */
118
+ drifted: string[];
119
+ rank: number;
120
+ }
121
+
122
+ export interface SweepResult {
123
+ scanned: number;
124
+ entries: EntryReport[];
125
+ stores: string[];
126
+ }
127
+
128
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
129
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
130
+ }
131
+
132
+ /** A 7-40 char lowercase hex string is the only accepted anchor shape. */
133
+ export function isCommitish(value: unknown): value is string {
134
+ return typeof value === 'string' && /^[0-9a-f]{7,40}$/.test(value);
135
+ }
136
+
137
+ /**
138
+ * Extract candidate pointers from one entry body.
139
+ *
140
+ * The grammar is deliberately narrow: backtick spans, `[[wiki-links]]`, and
141
+ * bare `ADR-NNN` ids. Prose outside backticks is never parsed — a sentence
142
+ * naming a file is not a pointer, and treating it as one is how a triage
143
+ * instrument starts manufacturing findings.
144
+ */
145
+ export function extractPointers(body: string): Array<{ raw: string; kind: PointerKind }> {
146
+ const out: Array<{ raw: string; kind: PointerKind }> = [];
147
+ const seen = new Set<string>();
148
+
149
+ const push = (raw: string, kind: PointerKind): void => {
150
+ const dedupKey = `${kind} ${raw}`;
151
+ if (seen.has(dedupKey)) return;
152
+ seen.add(dedupKey);
153
+ out.push({ raw, kind });
154
+ };
155
+
156
+ for (const m of body.matchAll(/\[\[([^\]]+)\]\]/g)) {
157
+ push((m[1] ?? '').trim(), 'wiki-link');
158
+ }
159
+ for (const m of body.matchAll(/\bADR-(\d{2,4})\b/g)) {
160
+ push(`ADR-${m[1] ?? ''}`, 'adr');
161
+ }
162
+ for (const m of body.matchAll(/`([^`\n]+)`/g)) {
163
+ const raw = (m[1] ?? '').trim();
164
+ if (!raw || /\s/.test(raw)) continue;
165
+ // Three shapes are NOT pointers and are dropped before classification,
166
+ // because flagging them is how a triage list stops being read:
167
+ // `<pack>/<name>/command.md` — a template, not a claim about a file
168
+ // `~/.claude/settings.json` — outside the repo, unresolvable here
169
+ // `/optimize-project` — a slash-command name, not a path
170
+ if (raw.includes('<') || raw.includes('>')) continue;
171
+ if (raw.includes('*')) continue; // a glob describes a set, not a file
172
+ if (raw.startsWith('~')) continue;
173
+ if (/^\/[A-Za-z][\w:-]*$/.test(raw)) continue;
174
+
175
+ const lineMatch = /^([\w./@+-]+):(\d+)(?:-\d+)?$/.exec(raw);
176
+ if (lineMatch && KNOWN_EXTENSIONS.has(path.extname(lineMatch[1] ?? ''))) {
177
+ push(raw, 'path-line');
178
+ continue;
179
+ }
180
+ // A trailing separator names a directory — mechanically resolvable and
181
+ // worth resolving: `src/domains/` disappearing is exactly the kind of
182
+ // structural move that invalidates a whole batch of entries at once.
183
+ if (raw.endsWith('/') && raw.length > 1) {
184
+ push(raw, 'dir');
185
+ continue;
186
+ }
187
+ // A bare path needs a separator AND a known extension. `--flag`,
188
+ // `some_function()`, and `task ci` are not paths and must not be
189
+ // ranked as dead ones.
190
+ if (raw.includes('/') && KNOWN_EXTENSIONS.has(path.extname(raw))) {
191
+ push(raw, 'path');
192
+ continue;
193
+ }
194
+ // Path-ish but unmatched: a separator and no recognised extension, or
195
+ // an extension and no separator. Reported as parser coverage, never
196
+ // resolved, and never ranked — see `rankOf`.
197
+ if (raw.includes('/') || KNOWN_EXTENSIONS.has(path.extname(raw))) {
198
+ push(raw, 'unparseable');
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+
204
+ /**
205
+ * Top-level names of the repo, cached. A pointer whose first segment is not
206
+ * one of them is a FRAGMENT (`analyze/decision.md`, `charge.ts:13`,
207
+ * `archive/`) rather than a repo-relative path, and resolving a fragment
208
+ * against the repo root manufactures a dead pointer out of a live claim.
209
+ *
210
+ * The trade-off is stated rather than hidden: this also demotes a genuinely
211
+ * dead reference to a top-level directory that ADR-051 retired from `dead` to
212
+ * `unparseable`, because its first segment no longer exists to match. A triage
213
+ * instrument must under-flag rather than over-flag — an unread queue catches
214
+ * nothing at all. (The retired directory is not named here: adding the literal
215
+ * anywhere under `src/` is what `check_no_new_legacy_path` forbids, and a
216
+ * docstring citing a dead path as an example is still a citation.)
217
+ */
218
+ let _repoRootNames: Set<string> | null = null;
219
+ function repoRootNames(): Set<string> {
220
+ if (_repoRootNames == null) {
221
+ try {
222
+ _repoRootNames = new Set(fs.readdirSync(REPO_ROOT));
223
+ } catch {
224
+ _repoRootNames = new Set();
225
+ }
226
+ }
227
+ return _repoRootNames;
228
+ }
229
+
230
+ /**
231
+ * basename → tracked paths, built once from `git ls-files`. Used to tell a
232
+ * relocated file from a deleted one.
233
+ */
234
+ let _basenameIndex: Map<string, string[]> | null = null;
235
+ function basenameIndex(): Map<string, string[]> {
236
+ if (_basenameIndex == null) {
237
+ _basenameIndex = new Map();
238
+ const res = spawnSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
239
+ if (res.status === 0 && typeof res.stdout === 'string') {
240
+ for (const line of res.stdout.split('\n')) {
241
+ const rel = line.trim();
242
+ if (!rel) continue;
243
+ const base = path.basename(rel);
244
+ const bucket = _basenameIndex.get(base);
245
+ if (bucket) bucket.push(rel);
246
+ else _basenameIndex.set(base, [rel]);
247
+ }
248
+ }
249
+ }
250
+ return _basenameIndex;
251
+ }
252
+
253
+ /**
254
+ * Is this path ignored by git?
255
+ *
256
+ * An ignored path (`agents/tmp/…`, `agents/runtime/council/responses/…`) is
257
+ * local state that is absent from every clone by design. Its absence says
258
+ * nothing about the entry that cites it, so reporting it as dead is a defect
259
+ * in the instrument rather than a finding about the store — and it was the
260
+ * single largest false-positive class in the first measured run (6 of 10).
261
+ */
262
+ function isGitIgnored(relPath: string): boolean {
263
+ const res = spawnSync('git', ['check-ignore', '-q', '--no-index', relPath], {
264
+ cwd: REPO_ROOT,
265
+ encoding: 'utf-8',
266
+ });
267
+ return res.status === 0;
268
+ }
269
+
270
+ function relocationsOf(relPath: string): string[] {
271
+ return (basenameIndex().get(path.basename(relPath)) ?? []).filter((p) => p !== relPath).sort();
272
+ }
273
+
274
+ function isRepoRooted(relPath: string): boolean {
275
+ const first = relPath.split('/')[0] ?? '';
276
+ return first.length > 0 && repoRootNames().has(first);
277
+ }
278
+
279
+ function fileLineCount(abs: string): number {
280
+ try {
281
+ const text = fs.readFileSync(abs, 'utf-8');
282
+ if (text === '') return 0;
283
+ const n = text.split('\n').length;
284
+ return text.endsWith('\n') ? n - 1 : n;
285
+ } catch {
286
+ return 0;
287
+ }
288
+ }
289
+
290
+ function resolveAdr(id: string): string | null {
291
+ const num = id.slice(4);
292
+ for (const dir of ['docs/decisions', 'docs/adr']) {
293
+ const abs = path.join(REPO_ROOT, dir);
294
+ let names: string[];
295
+ try {
296
+ names = fs.readdirSync(abs);
297
+ } catch {
298
+ continue;
299
+ }
300
+ const hit = names.find((n) => n.startsWith(`ADR-${num}-`) || n === `ADR-${num}.md`);
301
+ if (hit) return path.posix.join(dir, hit);
302
+ }
303
+ return null;
304
+ }
305
+
306
+ /** Resolve one candidate against the tree. Pure w.r.t. the store, not the FS. */
307
+ export function resolvePointer(
308
+ candidate: { raw: string; kind: PointerKind },
309
+ knownIds: ReadonlySet<string>,
310
+ ): Pointer {
311
+ const { raw, kind } = candidate;
312
+ if (kind === 'unparseable') {
313
+ return { raw, kind, state: 'unparseable', detail: 'no pointer class matched' };
314
+ }
315
+ if (kind === 'wiki-link') {
316
+ return knownIds.has(raw)
317
+ ? { raw, kind, state: 'live', target: raw }
318
+ : { raw, kind, state: 'dead', detail: 'no curated entry carries this id' };
319
+ }
320
+ if (kind === 'adr') {
321
+ const hit = resolveAdr(raw);
322
+ return hit
323
+ ? { raw, kind, state: 'live', target: hit }
324
+ : { raw, kind, state: 'dead', detail: 'no ADR file with this number' };
325
+ }
326
+ const [relPath, lineStr] = kind === 'path-line' ? raw.split(':') : [raw, null];
327
+ if (!isRepoRooted(relPath)) {
328
+ return { raw, kind, state: 'unparseable', detail: 'not repo-rooted — first segment is not a top-level name' };
329
+ }
330
+ const abs = path.join(REPO_ROOT, relPath);
331
+ if (!fs.existsSync(abs)) {
332
+ if (isGitIgnored(relPath)) {
333
+ return { raw, kind, state: 'unparseable', target: relPath, detail: 'gitignored — absent by design, not rotted' };
334
+ }
335
+ const moved = kind === 'dir' ? [] : relocationsOf(relPath);
336
+ if (moved.length > 0) {
337
+ return {
338
+ raw,
339
+ kind,
340
+ state: 'moved',
341
+ target: relPath,
342
+ detail: `now at ${moved.slice(0, 2).join(', ')}${moved.length > 2 ? ` (+${moved.length - 2})` : ''}`,
343
+ };
344
+ }
345
+ return { raw, kind, state: 'dead', target: relPath, detail: 'path does not exist' };
346
+ }
347
+ if (kind === 'dir' && !fs.statSync(abs).isDirectory()) {
348
+ return { raw, kind, state: 'dead', target: relPath, detail: 'exists but is not a directory' };
349
+ }
350
+ if (kind === 'path-line' && lineStr) {
351
+ const want = parseInt(lineStr, 10);
352
+ const have = fileLineCount(abs);
353
+ if (want > have) {
354
+ return {
355
+ raw,
356
+ kind,
357
+ state: 'dead',
358
+ target: relPath,
359
+ detail: `line ${want} is past end of file (${have} lines)`,
360
+ };
361
+ }
362
+ }
363
+ return { raw, kind, state: 'live', target: relPath };
364
+ }
365
+
366
+ /**
367
+ * Paths cited by the entry that changed between its anchor and HEAD.
368
+ *
369
+ * The anchor means "the revision this entry was semantically verified
370
+ * against", so drift is a re-verification prompt, never a staleness verdict —
371
+ * a file can change a hundred times without touching the claim.
372
+ */
373
+ function driftedPaths(anchor: string, targets: readonly string[]): string[] {
374
+ if (targets.length === 0) return [];
375
+ const res = spawnSync(
376
+ 'git',
377
+ ['diff', '--name-only', `${anchor}..HEAD`, '--', ...targets],
378
+ { cwd: REPO_ROOT, encoding: 'utf-8' },
379
+ );
380
+ if (res.status !== 0 || typeof res.stdout !== 'string') return [];
381
+ return res.stdout.split('\n').map((s) => s.trim()).filter(Boolean).sort();
382
+ }
383
+
384
+ function anchorResolves(anchor: string): boolean {
385
+ const res = spawnSync('git', ['cat-file', '-e', `${anchor}^{commit}`], {
386
+ cwd: REPO_ROOT,
387
+ encoding: 'utf-8',
388
+ });
389
+ return res.status === 0;
390
+ }
391
+
392
+ function readStore(storeDir: string): Array<{ file: string; type: string; entries: Array<Record<string, unknown>> }> {
393
+ const abs = path.isAbsolute(storeDir) ? storeDir : path.join(REPO_ROOT, storeDir);
394
+ let names: string[];
395
+ try {
396
+ names = fs.readdirSync(abs).filter((n) => n.endsWith('.yml') || n.endsWith('.yaml'));
397
+ } catch {
398
+ return [];
399
+ }
400
+ names.sort();
401
+ const out: Array<{ file: string; type: string; entries: Array<Record<string, unknown>> }> = [];
402
+ for (const name of names) {
403
+ const file = path.join(abs, name);
404
+ let parsed: unknown;
405
+ try {
406
+ parsed = YAML.parse(fs.readFileSync(file, 'utf-8'));
407
+ } catch {
408
+ continue;
409
+ }
410
+ const entries: Array<Record<string, unknown>> = [];
411
+ if (isPlainObject(parsed) && Array.isArray(parsed['entries'])) {
412
+ for (const item of parsed['entries'] as unknown[]) {
413
+ if (isPlainObject(item)) entries.push(item);
414
+ }
415
+ }
416
+ out.push({
417
+ file: path.relative(REPO_ROOT, file),
418
+ type: name.replace(/\.(ya?ml)$/, ''),
419
+ entries,
420
+ });
421
+ }
422
+ return out;
423
+ }
424
+
425
+ function bodyOf(entry: Record<string, unknown>): string {
426
+ const parts: string[] = [];
427
+ for (const f of ['body', 'rule', 'symptom', 'description', 'summary', 'key']) {
428
+ const v = entry[f];
429
+ if (typeof v === 'string' && v.trim()) parts.push(v);
430
+ }
431
+ return parts.join('\n');
432
+ }
433
+
434
+ function asString(v: unknown): string {
435
+ if (v == null) return '';
436
+ if (v instanceof Date) return v.toISOString().slice(0, 10);
437
+ return String(v);
438
+ }
439
+
440
+ /**
441
+ * Order entries by how much pointer repair they need. NOT a staleness score —
442
+ * that claim was measured at 0.00x lift and withdrawn (see the header).
443
+ *
444
+ * Dead citations dominate because they are the only unambiguous integrity
445
+ * defect. Drift and a missing anchor are weaker: they say the entry cannot be
446
+ * re-checked cheaply, not that anything about it is wrong.
447
+ *
448
+ * `unparseable` deliberately contributes NOTHING. It measures the grammar's
449
+ * coverage, not the entry's health, and the first run proved why: counting it
450
+ * put all 107 of 107 entries in the report, which is a list nobody reads.
451
+ */
452
+ export function rankOf(r: Omit<EntryReport, 'rank'>): number {
453
+ return r.dead * 100 + r.moved * 2 + r.drifted.length * 5 + (r.anchor_state === 'present' ? 0 : 1);
454
+ }
455
+
456
+ /**
457
+ * An entry is listed only on a structural defect — never on age, anchor state,
458
+ * or parser coverage.
459
+ *
460
+ * `moved` is deliberately outside this predicate. A cited path that now lives
461
+ * somewhere else is a documentation fix (re-anchor the citation), not a defect
462
+ * in the entry, and treating the two alike is what put the first measured
463
+ * precision below the base rate.
464
+ */
465
+ export function isFlagged(r: Pick<EntryReport, 'dead' | 'drifted'>): boolean {
466
+ return r.dead > 0 || r.drifted.length > 0;
467
+ }
468
+
469
+ export function sweep(storeDir: string): SweepResult {
470
+ const stores = readStore(storeDir);
471
+ const knownIds = new Set<string>();
472
+ for (const s of stores) {
473
+ for (const e of s.entries) {
474
+ const id = asString(e['id']);
475
+ if (id) knownIds.add(id);
476
+ }
477
+ }
478
+
479
+ const reports: EntryReport[] = [];
480
+ for (const s of stores) {
481
+ for (const e of s.entries) {
482
+ const candidates = extractPointers(bodyOf(e));
483
+ const pointers = candidates.map((c) => resolvePointer(c, knownIds));
484
+ const anchorRaw = e['verified_at_commit'];
485
+ let anchor_state: EntryReport['anchor_state'] = 'missing';
486
+ let verified_at_commit: string | null = null;
487
+ let drifted: string[] = [];
488
+ if (isCommitish(anchorRaw)) {
489
+ verified_at_commit = anchorRaw;
490
+ if (anchorResolves(anchorRaw)) {
491
+ anchor_state = 'present';
492
+ const targets = [
493
+ ...new Set(
494
+ pointers
495
+ .filter((p) => p.state === 'live' && p.target && p.kind !== 'wiki-link')
496
+ .map((p) => p.target as string),
497
+ ),
498
+ ].sort();
499
+ drifted = driftedPaths(anchorRaw, targets);
500
+ } else {
501
+ anchor_state = 'unresolvable';
502
+ }
503
+ } else if (anchorRaw != null) {
504
+ anchor_state = 'unresolvable';
505
+ }
506
+
507
+ const base: Omit<EntryReport, 'rank'> = {
508
+ id: asString(e['id']),
509
+ type: s.type,
510
+ status: asString(e['status']),
511
+ last_validated: asString(e['last_validated']),
512
+ review_after_days: typeof e['review_after_days'] === 'number' ? (e['review_after_days'] as number) : null,
513
+ verified_at_commit,
514
+ anchor_state,
515
+ pointers,
516
+ dead: pointers.filter((p) => p.state === 'dead').length,
517
+ moved: pointers.filter((p) => p.state === 'moved').length,
518
+ live: pointers.filter((p) => p.state === 'live').length,
519
+ unparseable: pointers.filter((p) => p.state === 'unparseable').length,
520
+ drifted,
521
+ };
522
+ reports.push({ ...base, rank: rankOf(base) });
523
+ }
524
+ }
525
+
526
+ reports.sort((a, b) => b.rank - a.rank || a.id.localeCompare(b.id));
527
+ return { scanned: reports.length, entries: reports, stores: stores.map((s) => s.file) };
528
+ }
529
+
530
+ function renderText(result: SweepResult, limit: number | null): string {
531
+ const lines: string[] = [];
532
+ const flagged = result.entries.filter((e) => isFlagged(e));
533
+ const sum = (pick: (e: EntryReport) => number): number => result.entries.reduce((n, e) => n + pick(e), 0);
534
+ const unparseable = sum((e) => e.unparseable);
535
+ const resolved = sum((e) => e.live) + sum((e) => e.dead) + sum((e) => e.moved);
536
+ const coverage = resolved + unparseable === 0 ? 100 : (resolved / (resolved + unparseable)) * 100;
537
+ lines.push(`${PROG} · pointer integrity, NOT staleness — measured lift over base rate: 0.00x (cf04)`);
538
+ lines.push(`scanned: ${result.scanned} entr(ies) across ${result.stores.length} store(s)`);
539
+ lines.push(
540
+ `flagged: ${flagged.length} · dead pointers: ${sum((e) => e.dead)}` +
541
+ ` · moved (re-anchor, not re-verify): ${sum((e) => e.moved)}` +
542
+ ` · missing anchor: ${result.entries.filter((e) => e.anchor_state !== 'present').length}`,
543
+ );
544
+ lines.push(`parser coverage: ${coverage.toFixed(1)}% (${resolved} resolved / ${unparseable} unparseable)`);
545
+ lines.push('');
546
+ const shown = limit == null ? flagged : flagged.slice(0, limit);
547
+ for (const e of shown) {
548
+ lines.push(` [${e.rank}] ${e.type}/${e.id}`);
549
+ for (const p of e.pointers.filter((x) => x.state === 'dead')) {
550
+ lines.push(` dead ${p.kind}: ${p.raw} — ${p.detail ?? ''}`);
551
+ }
552
+ for (const p of e.pointers.filter((x) => x.state === 'unparseable')) {
553
+ lines.push(` unparseable: ${p.raw}`);
554
+ }
555
+ if (e.drifted.length) {
556
+ lines.push(` drifted since ${e.verified_at_commit}: ${e.drifted.join(', ')}`);
557
+ }
558
+ if (e.anchor_state !== 'present') {
559
+ lines.push(` anchor ${e.anchor_state} — cannot tell a verified entry from a re-stamped one`);
560
+ }
561
+ }
562
+ if (limit != null && flagged.length > limit) {
563
+ lines.push(` … ${flagged.length - limit} more (raise --limit)`);
564
+ }
565
+ return lines.join('\n');
566
+ }
567
+
568
+ export function main(argv: readonly string[]): number {
569
+ let storeDir = DEFAULT_STORE;
570
+ let format = 'text';
571
+ let limit: number | null = null;
572
+ for (let i = 0; i < argv.length; i += 1) {
573
+ const a = argv[i];
574
+ if (a === '--path') storeDir = argv[++i] ?? DEFAULT_STORE;
575
+ else if (a === '--format') format = argv[++i] ?? 'text';
576
+ else if (a === '--limit') limit = parseInt(argv[++i] ?? '0', 10) || null;
577
+ else if (a === '--help' || a === '-h') {
578
+ process.stdout.write(
579
+ `usage: ${PROG} [--path DIR] [--format text|json] [--limit N]\n` +
580
+ `Pointer integrity of curated memory entries: dead, moved,\n` +
581
+ `unparseable citations plus anchor coverage. NOT a staleness\n` +
582
+ `signal — that claim measured 0.00x lift and was withdrawn.\n` +
583
+ `Never writes, never demotes, always exits 0.\n`,
584
+ );
585
+ return 0;
586
+ }
587
+ }
588
+
589
+ const result = sweep(storeDir);
590
+ try {
591
+ assertScanned({
592
+ gate: PROG,
593
+ scanned: result.scanned,
594
+ units: 'entr(ies)',
595
+ roots: [storeDir],
596
+ allowEmpty: 'a consumer install may carry no curated memory at all',
597
+ });
598
+ } catch (e) {
599
+ if (e instanceof DeadScopeError) {
600
+ process.stderr.write(`${e.message}\n`);
601
+ return 0;
602
+ }
603
+ throw e;
604
+ }
605
+
606
+ if (format === 'json') {
607
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
608
+ } else {
609
+ process.stdout.write(`${renderText(result, limit)}\n`);
610
+ }
611
+ return 0;
612
+ }
613
+
614
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
615
+ if (invokedDirectly) {
616
+ try {
617
+ process.exit(main(process.argv.slice(2)));
618
+ } catch (e) {
619
+ process.stderr.write(`${PROG}: internal error: ${(e as Error).message}\n`);
620
+ process.exit(3);
621
+ }
622
+ }
@@ -38,6 +38,7 @@ import {
38
38
  } from './_lib/self_repair.js';
39
39
  import { openRecords, upsertFinding } from './_lib/self_repair_store.js';
40
40
  import { readHookStdin } from './hooks/hook_stdin.js';
41
+ import { emitDefectShadow } from './hooks/telemetry_self_repair.js';
41
42
 
42
43
  const EXIT_ALLOW = 0;
43
44
 
@@ -130,11 +131,28 @@ export function readTurn(transcriptPath: string, pinned: 'de' | 'en' | null): Tu
130
131
  return { prompt, reply, toolCommands, pinnedLanguage: pinned };
131
132
  }
132
133
 
133
- /** The queue line injected at prompt time when records are waiting. */
134
- export function buildQueueLine(count: number, newest: string): string {
134
+ /**
135
+ * The queue line injected at prompt time when records are waiting.
136
+ *
137
+ * `occurrences` is the newest record's own repeat counter, and it is in the line
138
+ * because the store has always counted while the model never saw the number: a
139
+ * recurring defect increments one record instead of adding a queue entry, so
140
+ * `count` stays flat and the recurrence is invisible. Above 1 it is the only
141
+ * deterministic "this came back" signal an agent gets, which is what makes
142
+ * `decision-revisit-gate` § Recurrence executable rather than aspirational.
143
+ * Omitted at 1 — a first sighting has nothing to say.
144
+ */
145
+ export function buildQueueLine(count: number, newest: string, occurrences = 1): string {
146
+ const repeat =
147
+ occurrences > 1
148
+ ? ` The newest record has recurred ${occurrences} time(s): the earlier ` +
149
+ `disposition did not hold, so per decision-revisit-gate § Recurrence ` +
150
+ `reopen it and name which assumption broke — resolve on evidence, ` +
151
+ `never on the repetition count.`
152
+ : '';
135
153
  return (
136
154
  `<self-repair-queue>${count} open agent-config defect record(s) ` +
137
- `(newest: ${newest}). These are defects in the AGENT CONFIG, not in this ` +
155
+ `(newest: ${newest}).${repeat} These are defects in the AGENT CONFIG, not in this ` +
138
156
  `project. Per the self-repair rule: analyse the newest record, author the ` +
139
157
  `fix against agent-config, and prepare the release — the outward step ` +
140
158
  `(push + PR, or an issue) still needs the user's word this turn. ` +
@@ -185,7 +203,17 @@ export function main(): number {
185
203
 
186
204
  for (const f of findings) {
187
205
  try {
188
- upsertFinding(root, f, now);
206
+ const rec = upsertFinding(root, f, now);
207
+ // The Class-A shadow (road-to-org-telemetry Phase 5, step 5.1).
208
+ // Structural fields only — the record type has no member able to
209
+ // hold `f.evidence` or `f.suggested_surface`, which are the
210
+ // Class-B payload and ship only on per-case approval. Inert unless
211
+ // the org pack activated telemetry; a refused record (`null` from
212
+ // the creation cap) emits nothing, because there is no queued
213
+ // defect to shadow.
214
+ if (rec !== null) {
215
+ emitDefectShadow(root, rec, env);
216
+ }
189
217
  } catch {
190
218
  // an unwritable store must never cost the user their turn
191
219
  }
@@ -208,7 +236,11 @@ export function main(): number {
208
236
  `${JSON.stringify({
209
237
  decision: 'allow',
210
238
  reason: `self-repair: ${open.length} open record(s)`,
211
- context: buildQueueLine(open.length, open[0]!.defect_class),
239
+ context: buildQueueLine(
240
+ open.length,
241
+ open[0]!.defect_class,
242
+ open[0]!.occurrences,
243
+ ),
212
244
  })}\n`,
213
245
  );
214
246
  return EXIT_ALLOW;