@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +384 -3
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/decision.md +7 -0
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/optimize/project.md +10 -0
- package/dist/agent-src/commands/roadmap/process-full.md +132 -4
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +45 -2
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
- package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
- package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_conformance.js +459 -709
- package/dist/cli-delegate/cmd_doctor.js +6 -6
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +7 -6
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +7 -6
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +7 -6
- package/dist/cli-delegate/cmd_uninstall.js +9 -7
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +7 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +72 -29
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +3127 -2022
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +276 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1744 -1191
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/condense.js +13 -164
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/estate-count-budget.json +367 -3
- package/src/config/gate-violation-baselines.json +41 -41
- package/src/config/pack-size-budget.json +3 -2
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +19 -6
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +325 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +6 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +38 -8
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/scope_guard.sh +86 -4
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/_lib/subagent_capsule.ts +81 -1
- package/src/scripts/adr/regenerate_index.ts +13 -2
- package/src/scripts/adr_cite_check.ts +424 -0
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/check_adr_frontmatter.ts +187 -1
- package/src/scripts/check_references.ts +60 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_single_delivery.ts +517 -0
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +20 -170
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +887 -68
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +782 -20
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/install.sh +12 -2
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/memory_consolidation.ts +189 -0
- package/src/scripts/memory_eviction.ts +384 -0
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +78 -0
- package/src/scripts/report_memory_pointers.ts +622 -0
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +115 -15
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +7 -0
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- /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
|
-
/**
|
|
134
|
-
|
|
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})
|
|
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(
|
|
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;
|