@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,424 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* ADR cite-check — evaluate a decision BEFORE citing it as a reason not to act.
|
|
4
|
+
*
|
|
5
|
+
* The defect this closes is one of ordering, not of permission. Measured across
|
|
6
|
+
* 26 days of transcripts: in at least 13 passages a change was parked or refused
|
|
7
|
+
* citing a named ADR, and in zero cases did the agent refuse an explicit
|
|
8
|
+
* overturn instruction. The lock held, the owner had to notice and void it
|
|
9
|
+
* retroactively, and only then did the work resume. Nothing in the tree turned
|
|
10
|
+
* "this ADR blocks the change" into "is this ADR still true?".
|
|
11
|
+
*
|
|
12
|
+
* So this tool answers exactly that question, and only from what is mechanically
|
|
13
|
+
* decidable:
|
|
14
|
+
*
|
|
15
|
+
* - **status** — a `superseded` / `deprecated` ADR is not a live lock at all.
|
|
16
|
+
* This alone resolves a class of blockage with certainty. `rejected` is
|
|
17
|
+
* deliberately excluded: on an ADR it records a rejected PROPOSAL, so the
|
|
18
|
+
* rejection is the live decision.
|
|
19
|
+
* - **successors and predecessors** — `superseded_by` / `supersedes`, plus the
|
|
20
|
+
* `amended_by` / `amends` pair.
|
|
21
|
+
* - **amendment blocks in the body** — three unreconciled conventions exist in
|
|
22
|
+
* the corpus, so all three are matched.
|
|
23
|
+
* - **back-references** — other ADRs that name this one. A decision reopened
|
|
24
|
+
* by a later ADR that never linked back is invisible from the ADR's own file
|
|
25
|
+
* (measured: ADR-035 still asserts a rejection ADR-232 reopened).
|
|
26
|
+
* - **review_trigger** — verbatim, plus a state.
|
|
27
|
+
*
|
|
28
|
+
* `indeterminate` is a FIRST-CLASS trigger result, not a failure. Every
|
|
29
|
+
* `review_trigger` in the corpus is a semantic condition ("reopen when the
|
|
30
|
+
* capacity premise changes", "if a fifth band appears"), so forcing them to a
|
|
31
|
+
* boolean would convert uncertainty into either permission or blockage. Both
|
|
32
|
+
* council seats (2026-08-19) independently rejected building a machine-readable
|
|
33
|
+
* trigger grammar as a prerequisite for exactly this reason. An `indeterminate`
|
|
34
|
+
* result means: this may not be presented as an unqualified lock — route it.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
* ./scripts-run src/scripts/adr_cite_check ADR-211
|
|
38
|
+
* ./scripts-run src/scripts/adr_cite_check ADR-001 ADR-035 --json
|
|
39
|
+
*
|
|
40
|
+
* Exit codes: 0 every reference resolved · 1 at least one did not · 2 usage.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import * as fs from 'node:fs';
|
|
44
|
+
import * as path from 'node:path';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
|
|
47
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The surfaces a decision can live on. Six exist in this tree; this tool reads
|
|
52
|
+
* the two that carry numbered ADRs and can therefore be cited as `ADR-NNN`.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately NOT covered, stated rather than silently skipped:
|
|
55
|
+
* `docs/contracts/adr-*.md` (15 files) and `agents/settings/contexts/adr-*.md`
|
|
56
|
+
* (6) are named-slug contracts with their own status vocabulary (`Decided`,
|
|
57
|
+
* `locked`) and no ADR number, so a numeric reference can never resolve to one;
|
|
58
|
+
* `agents/decisions/` is a ledger of rows, not of files. A tool that silently
|
|
59
|
+
* saw one of six surfaces would report a real lock as unknown, which reads as
|
|
60
|
+
* "no constraint" and is worse than the stall it replaces — so the gap is
|
|
61
|
+
* printed in `--json` under `surfaces_not_scanned` and named here.
|
|
62
|
+
*
|
|
63
|
+
* The per-area surface took two passes to get honest. Its files are
|
|
64
|
+
* `<area>/NNNN-<slug>.md` with no `ADR-` prefix, so the flat filename pattern
|
|
65
|
+
* could never match one — the directory was listed as scanned and resolved
|
|
66
|
+
* nothing, which is the same false "not found" the paragraph above warns
|
|
67
|
+
* about, committed by this very file. It resolves by path and by the
|
|
68
|
+
* `ADR-<area>-NNNN` citation form now; a BARE number still cannot address one,
|
|
69
|
+
* because per-area numbering restarts per area and five files here are `0001`.
|
|
70
|
+
* See `PARTIAL_COVERAGE` for what a per-area hit does and does not carry.
|
|
71
|
+
*/
|
|
72
|
+
export const ADR_DIRS = ['docs/decisions', 'docs/adrs'] as const;
|
|
73
|
+
|
|
74
|
+
export const SURFACES_NOT_SCANNED = [
|
|
75
|
+
'docs/contracts/adr-*.md — slug-named contracts, no ADR number to resolve against',
|
|
76
|
+
'agents/settings/contexts/adr-*.md — shadow notes, own status vocabulary',
|
|
77
|
+
'agents/decisions/ — row ledger, not per-decision files',
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Partial coverage, stated because a silent partial is the failure this tool
|
|
82
|
+
* exists to avoid: `docs/adrs/<area>/` resolves by PATH and by the
|
|
83
|
+
* `ADR-<area>-NNNN` citation form, but those files carry a quote-block header
|
|
84
|
+
* rather than YAML frontmatter, so `status` / `review_trigger` / the link
|
|
85
|
+
* fields read as absent. A per-area result is an accurate location and an
|
|
86
|
+
* honestly empty metadata set — never a claim that the fields are empty in the
|
|
87
|
+
* document.
|
|
88
|
+
*/
|
|
89
|
+
export const PARTIAL_COVERAGE = [
|
|
90
|
+
'docs/adrs/<area>/ — resolves, but the header is a quote block, not YAML: metadata reads empty',
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
/** A trigger state. `none` means the ADR never recorded a reopen condition. */
|
|
94
|
+
export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired';
|
|
95
|
+
|
|
96
|
+
export interface CiteResult {
|
|
97
|
+
ref: string;
|
|
98
|
+
resolved: boolean;
|
|
99
|
+
file?: string;
|
|
100
|
+
status?: string;
|
|
101
|
+
date?: string;
|
|
102
|
+
decision?: string;
|
|
103
|
+
review_trigger?: string;
|
|
104
|
+
trigger_state: TriggerState;
|
|
105
|
+
supersedes?: string;
|
|
106
|
+
superseded_by?: string;
|
|
107
|
+
amends?: string;
|
|
108
|
+
amended_by?: string;
|
|
109
|
+
/** Amendment headings found in the body, any of the three conventions. */
|
|
110
|
+
amendment_blocks: string[];
|
|
111
|
+
/** Other ADR files whose text names this one. */
|
|
112
|
+
referenced_by: string[];
|
|
113
|
+
/** Why this may or may not be cited as a live lock. */
|
|
114
|
+
verdict: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A parsed citation. `area` is null for the flat surface.
|
|
119
|
+
*
|
|
120
|
+
* Per-area numbering restarts at `0001` inside every area, so a bare number
|
|
121
|
+
* cannot address one: five files in this tree are `0001`. A per-area citation
|
|
122
|
+
* must therefore name its area — `ADR-cost-0001`, the form `adr-layout.md`
|
|
123
|
+
* already specifies for cross-surface `supersedes:` values.
|
|
124
|
+
*/
|
|
125
|
+
export interface ParsedRef {
|
|
126
|
+
id: string;
|
|
127
|
+
area: string | null;
|
|
128
|
+
num: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `ADR-211` · `adr-211` · `211` · a path → the flat surface.
|
|
133
|
+
* `ADR-cost-0001` · a `docs/adrs/<area>/NNNN-…` path → that area.
|
|
134
|
+
*/
|
|
135
|
+
export function normalise_ref(ref: string): ParsedRef | null {
|
|
136
|
+
const raw = ref.trim();
|
|
137
|
+
|
|
138
|
+
// A per-area path carries its area in the directory, and its file never
|
|
139
|
+
// has an `ADR-` prefix — `docs/adrs/cost/0001-hard-stop-hook.md`.
|
|
140
|
+
const byPath = /(?:^|\/)docs\/adrs\/([a-z0-9-]+)\/(\d{1,4})-/i.exec(raw);
|
|
141
|
+
if (byPath?.[1] !== undefined && byPath[2] !== undefined) {
|
|
142
|
+
const num = byPath[2].padStart(4, '0');
|
|
143
|
+
return { id: `ADR-${byPath[1]}-${num}`, area: byPath[1], num };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// `ADR-<area>-NNNN` — the citation form for a per-area decision.
|
|
147
|
+
const byArea = /^adr-([a-z][a-z0-9-]*?)-(\d{1,4})$/i.exec(raw.replace(/\.md$/i, ''));
|
|
148
|
+
if (byArea?.[1] !== undefined && byArea[2] !== undefined) {
|
|
149
|
+
const area = byArea[1].toLowerCase();
|
|
150
|
+
const num = byArea[2].padStart(4, '0');
|
|
151
|
+
return { id: `ADR-${area}-${num}`, area, num };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const m = /(?:^|[^0-9])(\d{1,4})(?:[^0-9]|$)/.exec(raw.replace(/^.*\//, ''));
|
|
155
|
+
if (m?.[1] === undefined) return null;
|
|
156
|
+
return { id: `ADR-${m[1].padStart(3, '0')}`, area: null, num: String(Number(m[1])) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function walk_md(dir: string): string[] {
|
|
160
|
+
if (!fs.existsSync(dir)) return [];
|
|
161
|
+
const out: string[] = [];
|
|
162
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
163
|
+
const full = path.join(dir, entry.name);
|
|
164
|
+
if (entry.isDirectory()) out.push(...walk_md(full));
|
|
165
|
+
// INDEX.md is generated FROM these files and names every one of them, so
|
|
166
|
+
// leaving it in makes every ADR look referenced by one more decision
|
|
167
|
+
// than it is — noise that hides the single real back-reference.
|
|
168
|
+
else if (entry.name.endsWith('.md') && entry.name !== 'INDEX.md') out.push(full);
|
|
169
|
+
}
|
|
170
|
+
return out.sort();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Every candidate decision file across the scanned surfaces. */
|
|
174
|
+
export function adr_files(repo_root: string = REPO_ROOT): string[] {
|
|
175
|
+
return ADR_DIRS.flatMap((d) => walk_md(path.join(repo_root, d)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Minimal frontmatter reader — folded (`>-`) values are joined, which the
|
|
180
|
+
* corpus needs: every `review_trigger` in it is a folded multi-line string.
|
|
181
|
+
*/
|
|
182
|
+
export function parse_frontmatter(text: string): Record<string, string> | null {
|
|
183
|
+
if (!text.startsWith('---\n')) return null;
|
|
184
|
+
const end = text.indexOf('\n---\n', 4);
|
|
185
|
+
if (end === -1) return null;
|
|
186
|
+
const out: Record<string, string> = {};
|
|
187
|
+
let key: string | null = null;
|
|
188
|
+
for (const raw of text.slice(4, end).split('\n')) {
|
|
189
|
+
const line = raw.replace(/\s+$/, '');
|
|
190
|
+
if (!line || line.trimStart().startsWith('#')) continue;
|
|
191
|
+
if (/^\s/.test(line) && key !== null) {
|
|
192
|
+
out[key] = `${out[key]} ${line.trim()}`.trim();
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const idx = line.indexOf(':');
|
|
196
|
+
if (idx === -1) continue;
|
|
197
|
+
key = line.slice(0, idx).trim();
|
|
198
|
+
out[key] = line
|
|
199
|
+
.slice(idx + 1)
|
|
200
|
+
.trim()
|
|
201
|
+
.replace(/^["'](.*)["']$/, '$1')
|
|
202
|
+
.replace(/^>-?$/, '');
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The three amendment conventions the corpus actually uses. No convention is
|
|
209
|
+
* canonical yet, so all three are matched rather than one being assumed.
|
|
210
|
+
*/
|
|
211
|
+
export function amendment_blocks(body: string): string[] {
|
|
212
|
+
const found: string[] = [];
|
|
213
|
+
for (const line of body.split('\n')) {
|
|
214
|
+
if (/^#{2,3}\s+Amendment\b/i.test(line) || /^\*\*Amended\b/i.test(line)) {
|
|
215
|
+
found.push(line.trim().replace(/^#+\s*/, '').replace(/\*\*/g, ''));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A trigger's state, from what is mechanically decidable and nothing else.
|
|
223
|
+
*
|
|
224
|
+
* There is no clause that returns `fired` or `not-fired` from prose, and that
|
|
225
|
+
* absence is the design: every trigger in the corpus is semantic. The states
|
|
226
|
+
* exist in the type because a future structured trigger can carry them; today
|
|
227
|
+
* the honest answers are `none` and `indeterminate`.
|
|
228
|
+
*/
|
|
229
|
+
export function trigger_state(fm: Record<string, string>): TriggerState {
|
|
230
|
+
const t = (fm['review_trigger'] ?? '').trim();
|
|
231
|
+
if (t === '') return 'none';
|
|
232
|
+
return 'indeterminate';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The one-line answer to "may I cite this as a reason not to act?". */
|
|
236
|
+
export function verdict_for(r: Omit<CiteResult, 'verdict'>): string {
|
|
237
|
+
if (!r.resolved) return 'UNRESOLVED — no ADR file carries this number on a scanned surface.';
|
|
238
|
+
const status = (r.status ?? '').toLowerCase();
|
|
239
|
+
// `rejected` is deliberately NOT in this set. On an ADR it means "the
|
|
240
|
+
// proposal was rejected" — the record is a LIVE lock stating that
|
|
241
|
+
// rejection, not a dead decision. Folding it in with `superseded` would
|
|
242
|
+
// make the tool clear a lock that still binds, which is a worse failure
|
|
243
|
+
// than the stall it replaces. Found by running this tool over the eleven
|
|
244
|
+
// ADRs the transcripts show blocking work: ADR-054 came back "not a live
|
|
245
|
+
// lock" and it is one.
|
|
246
|
+
if (status === 'superseded' || status === 'deprecated') {
|
|
247
|
+
const by = r.superseded_by !== undefined && r.superseded_by !== '—' ? ` (by ${r.superseded_by})` : '';
|
|
248
|
+
return `NOT A LIVE LOCK — status is \`${status}\`${by}. Citing it as a blocker is a stale-state claim.`;
|
|
249
|
+
}
|
|
250
|
+
if (status === 'rejected') {
|
|
251
|
+
return (
|
|
252
|
+
'LIVE — status `rejected` records a REJECTED PROPOSAL, so the rejection is the ' +
|
|
253
|
+
'decision and it still binds. Reopening it means showing the premise it rejected on ' +
|
|
254
|
+
'has changed; it does not mean the record is stale.'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (r.amendment_blocks.length > 0 || (r.amended_by !== undefined && r.amended_by !== '—')) {
|
|
258
|
+
return 'AMENDED — read the amendment before citing the original decision; the text above it may no longer hold.';
|
|
259
|
+
}
|
|
260
|
+
if (r.trigger_state === 'none') {
|
|
261
|
+
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.';
|
|
262
|
+
}
|
|
263
|
+
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.';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function cite_check(refs: string[], repo_root: string = REPO_ROOT): CiteResult[] {
|
|
267
|
+
const files = adr_files(repo_root);
|
|
268
|
+
const contents = new Map<string, string>();
|
|
269
|
+
for (const f of files) contents.set(f, fs.readFileSync(f, 'utf-8'));
|
|
270
|
+
|
|
271
|
+
return refs.map((ref) => {
|
|
272
|
+
const parsed = normalise_ref(ref);
|
|
273
|
+
if (parsed === null) {
|
|
274
|
+
const empty: Omit<CiteResult, 'verdict'> = {
|
|
275
|
+
ref,
|
|
276
|
+
resolved: false,
|
|
277
|
+
trigger_state: 'none',
|
|
278
|
+
amendment_blocks: [],
|
|
279
|
+
referenced_by: [],
|
|
280
|
+
};
|
|
281
|
+
return { ...empty, verdict: verdict_for(empty) };
|
|
282
|
+
}
|
|
283
|
+
const { id, area, num } = parsed;
|
|
284
|
+
|
|
285
|
+
// Two filename conventions, and the flat one's `ADR-` prefix is exactly
|
|
286
|
+
// what a per-area file does NOT have (`docs/adrs/cost/0001-…`). Matching
|
|
287
|
+
// both against one pattern was the defect: the tool advertised
|
|
288
|
+
// `docs/adrs` as covered and could never resolve a citation to it.
|
|
289
|
+
const match =
|
|
290
|
+
area === null
|
|
291
|
+
? files.find((f) => new RegExp(`(^|/)ADR-0*${num}[-.]`, 'i').test(f))
|
|
292
|
+
: files.find((f) =>
|
|
293
|
+
new RegExp(`/docs/adrs/${area}/0*${Number(num)}-`, 'i').test(f),
|
|
294
|
+
);
|
|
295
|
+
if (match === undefined) {
|
|
296
|
+
const empty: Omit<CiteResult, 'verdict'> = {
|
|
297
|
+
ref: id,
|
|
298
|
+
resolved: false,
|
|
299
|
+
trigger_state: 'none',
|
|
300
|
+
amendment_blocks: [],
|
|
301
|
+
referenced_by: [],
|
|
302
|
+
};
|
|
303
|
+
return { ...empty, verdict: verdict_for(empty) };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const text = contents.get(match) ?? '';
|
|
307
|
+
const fm = parse_frontmatter(text) ?? {};
|
|
308
|
+
const bodyStart = text.indexOf('\n---\n', 4);
|
|
309
|
+
const body = bodyStart === -1 ? text : text.slice(bodyStart + 5);
|
|
310
|
+
|
|
311
|
+
// Back-references: any OTHER decision file naming this number. This is
|
|
312
|
+
// the only way to see a one-sided reopen — the amending ADR links back,
|
|
313
|
+
// the amended one does not.
|
|
314
|
+
// A per-area decision is cited as `ADR-<area>-NNNN`; a flat one by number.
|
|
315
|
+
const cited =
|
|
316
|
+
area === null
|
|
317
|
+
? new RegExp(`ADR-0*${num}\\b`, 'i')
|
|
318
|
+
: new RegExp(`ADR-${area}-0*${Number(num)}\\b`, 'i');
|
|
319
|
+
const referenced_by = files
|
|
320
|
+
.filter((f) => f !== match && cited.test(contents.get(f) ?? ''))
|
|
321
|
+
.map((f) => path.relative(repo_root, f));
|
|
322
|
+
|
|
323
|
+
const partial: Omit<CiteResult, 'verdict'> = {
|
|
324
|
+
ref: id,
|
|
325
|
+
resolved: true,
|
|
326
|
+
file: path.relative(repo_root, match),
|
|
327
|
+
...(fm['status'] !== undefined ? { status: fm['status'] } : {}),
|
|
328
|
+
...(fm['date'] !== undefined ? { date: fm['date'] } : {}),
|
|
329
|
+
...(fm['decision'] !== undefined ? { decision: fm['decision'] } : {}),
|
|
330
|
+
...(fm['review_trigger'] !== undefined && fm['review_trigger'] !== ''
|
|
331
|
+
? { review_trigger: fm['review_trigger'] }
|
|
332
|
+
: {}),
|
|
333
|
+
trigger_state: trigger_state(fm),
|
|
334
|
+
...(fm['supersedes'] !== undefined ? { supersedes: fm['supersedes'] } : {}),
|
|
335
|
+
...(fm['superseded_by'] !== undefined ? { superseded_by: fm['superseded_by'] } : {}),
|
|
336
|
+
...(fm['amends'] !== undefined ? { amends: fm['amends'] } : {}),
|
|
337
|
+
...(fm['amended_by'] !== undefined ? { amended_by: fm['amended_by'] } : {}),
|
|
338
|
+
amendment_blocks: amendment_blocks(body),
|
|
339
|
+
referenced_by,
|
|
340
|
+
};
|
|
341
|
+
return { ...partial, verdict: verdict_for(partial) };
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function render(results: CiteResult[]): string {
|
|
346
|
+
const lines: string[] = [];
|
|
347
|
+
for (const r of results) {
|
|
348
|
+
lines.push(`\n${r.ref}${r.file !== undefined ? ` · ${r.file}` : ''}`);
|
|
349
|
+
if (!r.resolved) {
|
|
350
|
+
lines.push(` ❌ ${r.verdict}`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
lines.push(` status ${r.status ?? '—'} date ${r.date ?? '—'}`);
|
|
354
|
+
if (r.decision !== undefined) lines.push(` decision ${r.decision}`);
|
|
355
|
+
lines.push(` review_trigger ${r.review_trigger ?? '— (none recorded)'}`);
|
|
356
|
+
lines.push(` trigger state ${r.trigger_state}`);
|
|
357
|
+
if (r.supersedes !== undefined && r.supersedes !== '—') lines.push(` supersedes ${r.supersedes}`);
|
|
358
|
+
if (r.superseded_by !== undefined && r.superseded_by !== '—')
|
|
359
|
+
lines.push(` superseded_by ${r.superseded_by}`);
|
|
360
|
+
if (r.amends !== undefined && r.amends !== '—') lines.push(` amends ${r.amends}`);
|
|
361
|
+
if (r.amended_by !== undefined && r.amended_by !== '—') lines.push(` amended_by ${r.amended_by}`);
|
|
362
|
+
if (r.amendment_blocks.length > 0)
|
|
363
|
+
lines.push(` amendments ${String(r.amendment_blocks.length)}: ${r.amendment_blocks.join(' · ')}`);
|
|
364
|
+
if (r.referenced_by.length > 0)
|
|
365
|
+
lines.push(` referenced by ${String(r.referenced_by.length)} other ADR(s): ${r.referenced_by.join(', ')}`);
|
|
366
|
+
lines.push(` → ${r.verdict}`);
|
|
367
|
+
}
|
|
368
|
+
return lines.join('\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function main(argv: string[]): number {
|
|
372
|
+
const as_json = argv.includes('--json');
|
|
373
|
+
const refs = argv.filter((a) => !a.startsWith('--'));
|
|
374
|
+
if (refs.length === 0) {
|
|
375
|
+
process.stderr.write(
|
|
376
|
+
'usage: adr_cite_check <ADR-NNN> [ADR-NNN …] [--json]\n' +
|
|
377
|
+
' evaluate a decision before citing it as a reason not to act\n',
|
|
378
|
+
);
|
|
379
|
+
return 2;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const results = cite_check(refs);
|
|
383
|
+
if (as_json) {
|
|
384
|
+
process.stdout.write(
|
|
385
|
+
JSON.stringify(
|
|
386
|
+
{
|
|
387
|
+
results,
|
|
388
|
+
surfaces_not_scanned: SURFACES_NOT_SCANNED,
|
|
389
|
+
partial_coverage: PARTIAL_COVERAGE,
|
|
390
|
+
},
|
|
391
|
+
null,
|
|
392
|
+
2,
|
|
393
|
+
) + '\n',
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
process.stdout.write(render(results) + '\n');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const unresolved = results.filter((r) => !r.resolved);
|
|
400
|
+
if (unresolved.length > 0) {
|
|
401
|
+
process.stderr.write(
|
|
402
|
+
`\n❌ ${String(unresolved.length)} reference(s) did not resolve on a scanned surface. ` +
|
|
403
|
+
`Not scanned: ${SURFACES_NOT_SCANNED.join(' · ')}\n`,
|
|
404
|
+
);
|
|
405
|
+
return 1;
|
|
406
|
+
}
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Main-guard (realpath-compared, mirrors the repo convention).
|
|
411
|
+
if (process.argv[1] !== undefined) {
|
|
412
|
+
try {
|
|
413
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
414
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
415
|
+
if (here === argv1) {
|
|
416
|
+
process.exit(main(process.argv.slice(2)));
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
420
|
+
if (import.meta.url === argvUrl) {
|
|
421
|
+
process.exit(main(process.argv.slice(2)));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-help rendering for the council CLI.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than in `council_cli.ts` for a measured reason: that file
|
|
5
|
+
* sits ~2,500 lines above the 1,500-line source ceiling, and
|
|
6
|
+
* `check_source_size_budget` is a shrink-only ratchet — every line added there
|
|
7
|
+
* is a new violation, so a 45-line help renderer reds the build. Extraction is
|
|
8
|
+
* the documented response; raising the baseline is not.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT FIXES. `council run --help` printed `usage: agent-config council run
|
|
11
|
+
* [-h] ...` and no flag list at all. Measured 2026-08-20: a first council run
|
|
12
|
+
* cost six calls, two of them spent discovering `--confirm` and `--output` by
|
|
13
|
+
* grepping the CLI source. The information was available at runtime all along —
|
|
14
|
+
* the parser's own spec table carries it — it simply was not printed.
|
|
15
|
+
*
|
|
16
|
+
* Rendered from those specs rather than hand-listed on purpose: a hand-written
|
|
17
|
+
* flag list is a second source of truth that drifts the first time a flag is
|
|
18
|
+
* added.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The shape `council_cli`'s spec table exposes, narrowed to what help needs. */
|
|
22
|
+
export interface HelpSpec {
|
|
23
|
+
readonly positionals: readonly string[];
|
|
24
|
+
readonly opts: readonly { readonly flag: string; readonly takesValue: boolean; readonly choices?: readonly string[] | null }[];
|
|
25
|
+
readonly requiredOpts: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `usageLine` first (its exact text is asserted elsewhere and must not move),
|
|
30
|
+
* then positionals, then every flag with its value shape and a `[required]`
|
|
31
|
+
* marker where the parser demands one.
|
|
32
|
+
*/
|
|
33
|
+
export function renderSubHelp(_cmd: string, usageLine: string, spec: HelpSpec): string {
|
|
34
|
+
const lines: string[] = [usageLine.trimEnd()];
|
|
35
|
+
if (spec.positionals.length > 0) {
|
|
36
|
+
lines.push('', 'positional arguments:');
|
|
37
|
+
for (const p of spec.positionals) {
|
|
38
|
+
lines.push(` ${p}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (spec.opts.length > 0) {
|
|
42
|
+
lines.push('', 'options:');
|
|
43
|
+
const required = new Set(spec.requiredOpts);
|
|
44
|
+
for (const o of spec.opts) {
|
|
45
|
+
const value = o.takesValue
|
|
46
|
+
? o.choices && o.choices.length > 0
|
|
47
|
+
? ` {${o.choices.join(',')}}`
|
|
48
|
+
: ' VALUE'
|
|
49
|
+
: '';
|
|
50
|
+
lines.push(` ${o.flag}${value}${required.has(o.flag) ? ' [required]' : ''}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push('');
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|