@event4u/agent-config 14.8.0 → 14.10.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 +214 -237
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/pr/merge.md +9 -5
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +36 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/skills/async-python-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/code-refactoring/SKILL.md +1 -1
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/discovery-interview/SKILL.md +1 -1
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +1 -1
- package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/editorial-calendar/SKILL.md +1 -1
- package/dist/agent-src/skills/github-ci/SKILL.md +1 -1
- package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-artifact-completeness/SKILL.md +1 -1
- package/dist/agent-src/skills/judge-injection-defense/SKILL.md +1 -1
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/laravel-validation/SKILL.md +1 -1
- package/dist/agent-src/skills/legal-intake-triage/SKILL.md +1 -1
- package/dist/agent-src/skills/legal-practice-profile/SKILL.md +1 -1
- package/dist/agent-src/skills/messaging-architecture/SKILL.md +1 -1
- package/dist/agent-src/skills/migration-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +1 -1
- package/dist/agent-src/skills/module-management/SKILL.md +1 -1
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +1 -1
- package/dist/agent-src/skills/onboarding-program/SKILL.md +1 -1
- package/dist/agent-src/skills/perf-feedback-craft/SKILL.md +1 -1
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +1 -1
- package/dist/agent-src/skills/receiving-code-review/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/skills/scene-expander/SKILL.md +1 -1
- package/dist/agent-src/skills/secrets-management/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +8 -0
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-MMSGVZKV.js → chunk-77GR3VD4.js} +65 -40
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-EEJUMCDZ.js → chunk-PLSG6UL5.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +89 -13
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +64 -42
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +562 -204
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +808 -331
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.js.map +1 -0
- package/dist/scripts/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/config/estate-count-budget.json +13 -744
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +267 -9
- package/src/config/gate-violation-baselines.json +72 -72
- package/src/config/preamble-payload-budget.json +71 -67
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +8 -1
- package/src/scripts/_dispatch.bash +7 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/regenerate_index.ts +14 -7
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/seating.ts +211 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +33 -6
- package/src/scripts/check_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_passport_reconciliation.ts +274 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/generate_pack_manifests.ts +78 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +19 -1
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +120 -10
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/schemas/pack.schema.json +62 -4
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/sync_pr_branch.ts +14 -7
- package/src/scripts/tighten_skill_descriptions.ts +264 -0
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- /package/dist/cli-delegate/{chunk-N7CDDGI3.js → chunk-CYH5SP6M.js} +0 -0
|
@@ -120,11 +120,65 @@ export function normalizeHostManifest(input: unknown): HostCapabilityManifest {
|
|
|
120
120
|
* host identity a hook/dispatch envelope carries).
|
|
121
121
|
*
|
|
122
122
|
* Each row is partial: an omitted field stays at the `SAFE_DEFAULT` `false`
|
|
123
|
-
* when the row is applied
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
123
|
+
* when the row is applied — and an ABSENT field is the record for "never
|
|
124
|
+
* looked", which is a different state from a field written as `false`
|
|
125
|
+
* deliberately. Add a field only once it is itself observed, never by
|
|
126
|
+
* inference.
|
|
127
|
+
*
|
|
128
|
+
* ## Rows are written under the observation protocol
|
|
129
|
+
*
|
|
130
|
+
* `contexts/execution/host-capability-manifest.md` § Observation protocol states
|
|
131
|
+
* the per-field criterion and the four-part citation (host · host version ·
|
|
132
|
+
* transcript or artefact reference · date). A row without all four is not
|
|
133
|
+
* admissible, and no row may be filled from a host's documentation.
|
|
134
|
+
*
|
|
135
|
+
* ## REACHABILITY, 2026-08-22 — one of eight, and the other seven are recorded
|
|
136
|
+
*
|
|
137
|
+
* `hook_manifest.yaml` declares eight platform keys. Reachable means a real
|
|
138
|
+
* session can be started on the host, not that it is installable in principle.
|
|
139
|
+
*
|
|
140
|
+
* - `claude` — REACHABLE. Row below, re-cited under the protocol.
|
|
141
|
+
* - `augment` — not reachable from this session: a different editor host, no
|
|
142
|
+
* session available to the running agent.
|
|
143
|
+
* - `cowork` — not reachable from this session, same reason.
|
|
144
|
+
* - `cursor` — not reachable from this session, same reason.
|
|
145
|
+
* - `cline` — not reachable from this session, same reason.
|
|
146
|
+
* - `windsurf` — not reachable from this session, same reason.
|
|
147
|
+
* - `gemini` — not reachable from this session, same reason.
|
|
148
|
+
* - `copilot` — not reachable from this session, same reason; additionally
|
|
149
|
+
* `fallback_only` in the manifest.
|
|
150
|
+
*
|
|
151
|
+
* The seven have **no row**, so every field resolves to the safe default and
|
|
152
|
+
* `describeHostCapabilities` reports its source as `default`. That is the
|
|
153
|
+
* "never looked" state, and it is the honest one — a recorded silence rather
|
|
154
|
+
* than a table with gaps that reads as a table of negatives.
|
|
155
|
+
*
|
|
156
|
+
* ## The `claude` row, RE-CITED rather than grandfathered
|
|
157
|
+
*
|
|
158
|
+
* The row predated the protocol. Both its fields are now carried on transcript
|
|
159
|
+
* evidence, so it is brought up to the new shape rather than marked
|
|
160
|
+
* grandfathered — which was the other permitted disposition and would have left
|
|
161
|
+
* it looking like it passed a protocol it never ran.
|
|
162
|
+
*
|
|
163
|
+
* `subagent_spawn: true` — OBSERVED. claude (Claude Code, Opus 5 1M session,
|
|
164
|
+
* 2026-08-22). `agents/runtime/state/subagent-ledger/2026-08.jsonl`: **445**
|
|
165
|
+
* `subagent_start` records, and **420** stops carrying a measured
|
|
166
|
+
* `duration_ms` — i.e. 420 child legs whose start and stop pair, which is the
|
|
167
|
+
* criterion (a measurable child duration, not an inferred one).
|
|
168
|
+
*
|
|
169
|
+
* `parallel_spawn: true` — OBSERVED. Same host, session and artefact.
|
|
170
|
+
* **6,168** of 6,388 stop records carry `concurrent_open >= 2`, with a
|
|
171
|
+
* maximum of **30** concurrently open children. Overlapping intervals at one
|
|
172
|
+
* instant, which is the criterion — not two children in sequence.
|
|
173
|
+
*
|
|
174
|
+
* `status_polling`, `separate_quota_pool`, `worker_respawn` — **absent from the
|
|
175
|
+
* row, and that is deliberate.** The ledger does not instrument any of the
|
|
176
|
+
* three, so this is "never looked", NOT "observed absent". They stay at the
|
|
177
|
+
* safe default and report `default` provenance. Writing any of them as an
|
|
178
|
+
* explicit `false` would claim a measurement nobody took.
|
|
179
|
+
*
|
|
180
|
+
* `agent_teams` — out of scope for a row by construction; see its own field
|
|
181
|
+
* doc comment and the protocol section.
|
|
128
182
|
*/
|
|
129
183
|
const HOST_CAPABILITY_REGISTRY: Readonly<Record<string, Partial<HostCapabilityManifest>>> = {
|
|
130
184
|
claude: { subagent_spawn: true, parallel_spawn: true },
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quota_parked — the marker that says a run stopped because plan quota ran out,
|
|
3
|
+
* rather than because it crashed.
|
|
4
|
+
*
|
|
5
|
+
* `run_supervise` can already tell a dead session from a live one, and a
|
|
6
|
+
* finished run from an unfinished one. What it cannot tell is WHY a run
|
|
7
|
+
* stopped, and for one cause that distinction decides the whole response: a run
|
|
8
|
+
* held back by an exhausted plan quota is not broken, it is early. Reported as
|
|
9
|
+
* `relaunchable` with "the session is gone" it reads as a crash, and the one
|
|
10
|
+
* fact an operator needs — that waiting is a strategy here and is not a
|
|
11
|
+
* strategy for a crash — is the fact that gets lost.
|
|
12
|
+
*
|
|
13
|
+
* **The trigger is ours, and that is the point.** Detecting the billing cliff
|
|
14
|
+
* inside Claude Code needs a host signal nobody has shown exists
|
|
15
|
+
* (`agents/roadmaps/later/road-to-billing-cliff-detection.md`). But the council
|
|
16
|
+
* establishes the same fact under its own roof: when `api_on_quota: 'ask'`
|
|
17
|
+
* parks a seat, plan quota for that provider is exhausted, measured by us, with
|
|
18
|
+
* no host surface involved. This marker is written from there.
|
|
19
|
+
*
|
|
20
|
+
* PII-exclusion-by-construction, the same shape as `billing_grant`: a record
|
|
21
|
+
* holds a run id, provider names, and an ISO stamp. There is no field capable
|
|
22
|
+
* of carrying a prompt, a path, or an error body, so there is no scrubbing pass
|
|
23
|
+
* here that could fail.
|
|
24
|
+
*
|
|
25
|
+
* **It records, it does not act.** Nothing here sleeps, probes, or relaunches.
|
|
26
|
+
* Unattended relaunch is a published refusal
|
|
27
|
+
* (road-to-long-horizon-execution 4.0, AI council 2026-08-19) whose reopen
|
|
28
|
+
* condition has since fired — see
|
|
29
|
+
* `agents/evidence/billing-cliff/spawn-reopen-condition.md` — and a fired
|
|
30
|
+
* condition reopens the question in the venue that closed it, never the
|
|
31
|
+
* capability by itself.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
|
|
37
|
+
/** Directory, relative to the repo root, holding one marker per parked run. */
|
|
38
|
+
export const QUOTA_PARKED_DIR_REL = path.join('agents', 'runtime', 'state', 'quota-parked');
|
|
39
|
+
|
|
40
|
+
export interface QuotaParkedMarker {
|
|
41
|
+
readonly run_id: string;
|
|
42
|
+
/** Providers whose plan quota was exhausted, sorted and deduplicated. */
|
|
43
|
+
readonly providers: string[];
|
|
44
|
+
readonly parked_at: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Filesystem-safe path for a run's marker. Mirrors `billing_grant.grantFile`. */
|
|
48
|
+
export function markerFile(repoRoot: string, runId: string): string {
|
|
49
|
+
return path.join(
|
|
50
|
+
repoRoot,
|
|
51
|
+
QUOTA_PARKED_DIR_REL,
|
|
52
|
+
`${runId.replace(/[^A-Za-z0-9_-]/g, '_')}.json`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record that `runId` is held back by exhausted plan quota on `providers`.
|
|
58
|
+
*
|
|
59
|
+
* Idempotent on the timestamp: a second park inside one run keeps the original
|
|
60
|
+
* `parked_at` and merges the provider set, so "when did this run first hit the
|
|
61
|
+
* cliff" survives a round that parks twice.
|
|
62
|
+
*/
|
|
63
|
+
export function writeQuotaParked(
|
|
64
|
+
repoRoot: string,
|
|
65
|
+
runId: string,
|
|
66
|
+
providers: readonly string[],
|
|
67
|
+
now: () => Date = () => new Date(),
|
|
68
|
+
): QuotaParkedMarker {
|
|
69
|
+
const file = markerFile(repoRoot, runId);
|
|
70
|
+
const existing = readMarker(file);
|
|
71
|
+
const merged = [...new Set([...(existing?.providers ?? []), ...providers])].sort();
|
|
72
|
+
const marker: QuotaParkedMarker = {
|
|
73
|
+
run_id: runId,
|
|
74
|
+
providers: merged,
|
|
75
|
+
parked_at: existing?.parked_at ?? now().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
78
|
+
fs.writeFileSync(file, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
|
|
79
|
+
return marker;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Remove a run's marker. Safe to call when none exists. */
|
|
83
|
+
export function clearQuotaParked(repoRoot: string, runId: string): void {
|
|
84
|
+
try {
|
|
85
|
+
fs.unlinkSync(markerFile(repoRoot, runId));
|
|
86
|
+
} catch {
|
|
87
|
+
// Already gone is the state the caller asked for.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The marker for a session, or `null`.
|
|
93
|
+
*
|
|
94
|
+
* Looks up by run id first, then scans. The scan is not redundant: the run id
|
|
95
|
+
* the council writes comes from `currentRunId`, whose chain is
|
|
96
|
+
* `AC_BILLING_GRANT` → `AC_RUN_ID` → `CLAUDE_CODE_SESSION_ID`, while the
|
|
97
|
+
* register's `session_id` comes from `AGENT_CONFIG_SESSION_ID` →
|
|
98
|
+
* `CLAUDE_CODE_SESSION_ID`. The two coincide in an ordinary Claude Code session
|
|
99
|
+
* and diverge the moment an orchestrator sets `AC_RUN_ID` — so a direct hit is
|
|
100
|
+
* the common case and the scan is what stops the uncommon one from silently
|
|
101
|
+
* reporting a parked run as a crashed one. The directory holds one small file
|
|
102
|
+
* per parked run, so the scan is cheap by construction.
|
|
103
|
+
*/
|
|
104
|
+
export function findQuotaParked(repoRoot: string, sessionId: string): QuotaParkedMarker | null {
|
|
105
|
+
const direct = readMarker(markerFile(repoRoot, sessionId));
|
|
106
|
+
if (direct !== null) return direct;
|
|
107
|
+
let names: string[];
|
|
108
|
+
try {
|
|
109
|
+
names = fs.readdirSync(path.join(repoRoot, QUOTA_PARKED_DIR_REL));
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
if (!name.endsWith('.json')) continue;
|
|
115
|
+
const m = readMarker(path.join(repoRoot, QUOTA_PARKED_DIR_REL, name));
|
|
116
|
+
if (m !== null && m.run_id === sessionId) return m;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readMarker(file: string): QuotaParkedMarker | null {
|
|
122
|
+
try {
|
|
123
|
+
const raw: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
124
|
+
if (raw === null || typeof raw !== 'object') return null;
|
|
125
|
+
const o = raw as Record<string, unknown>;
|
|
126
|
+
if (typeof o['run_id'] !== 'string' || typeof o['parked_at'] !== 'string') return null;
|
|
127
|
+
if (!Array.isArray(o['providers'])) return null;
|
|
128
|
+
const providers = o['providers'].filter((p): p is string => typeof p === 'string');
|
|
129
|
+
return { run_id: o['run_id'], providers, parked_at: o['parked_at'] };
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -85,6 +85,68 @@ export function sanitize_text(s: string): string {
|
|
|
85
85
|
return out.length > MAX_FIELD_CHARS ? out.slice(0, MAX_FIELD_CHARS) : out;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Report the INVISIBLE layer without removing it.
|
|
90
|
+
*
|
|
91
|
+
* `sanitize_text` above strips four channels — hidden-instruction vectors
|
|
92
|
+
* (bidi, zero-width, tag, deprecated-format, private-use-area, per `_classify`),
|
|
93
|
+
* strippable control chars, and invisible fillers. Stripping is the right
|
|
94
|
+
* disposition for a retrieval path that is about to hand text to a model.
|
|
95
|
+
*
|
|
96
|
+
* It is the WRONG disposition for a reporting surface. A warn-only PostToolUse
|
|
97
|
+
* hook must not rewrite what the agent already read, so it cannot call
|
|
98
|
+
* `sanitize_text` at all — and `scan_encoding_findings` is the VISIBLE-layer
|
|
99
|
+
* twin and deliberately says nothing about these four. Measured over the frozen
|
|
100
|
+
* corpus before this function existed: `deprecated-format`, `private-use-area`,
|
|
101
|
+
* `control-char` and `invisible-filler` reported **0 of 20 each** through the
|
|
102
|
+
* reporting API while `sanitize_text` changed **20 of 20** for every one of
|
|
103
|
+
* them. So the layer had a reporting blind spot exactly where its stripping was
|
|
104
|
+
* perfect, and a consumer that may not strip was blind to a quarter of the
|
|
105
|
+
* channel set.
|
|
106
|
+
*
|
|
107
|
+
* The published 99.00 % recall figure is the STRIPPING pipeline's. The reporting
|
|
108
|
+
* half alone measured 72.33 %. Both numbers are correct about different things,
|
|
109
|
+
* and conflating them is what made "import the measured layer and get 15
|
|
110
|
+
* channels" read as true.
|
|
111
|
+
*
|
|
112
|
+
* Additive by construction: nothing that exists changes behaviour, and the
|
|
113
|
+
* predicates are the SAME ones `sanitize_text` uses — not copies — so a channel
|
|
114
|
+
* cannot become strippable without becoming reportable in the same edit.
|
|
115
|
+
*/
|
|
116
|
+
export function scan_invisible_findings(s: string): EncodingFinding[] {
|
|
117
|
+
const out: EncodingFinding[] = [];
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
const add = (channel: string, cp: number): void => {
|
|
120
|
+
const key = `${channel}:${cp}`;
|
|
121
|
+
if (seen.has(key)) return;
|
|
122
|
+
seen.add(key);
|
|
123
|
+
out.push({
|
|
124
|
+
channel,
|
|
125
|
+
detail: `U+${cp.toString(16).toUpperCase().padStart(4, '0')} in the invisible layer`,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
for (const ch of s) {
|
|
129
|
+
const cp = ch.codePointAt(0);
|
|
130
|
+
if (cp === undefined) continue;
|
|
131
|
+
const classified = _classify(cp);
|
|
132
|
+
if (classified !== null) {
|
|
133
|
+
// `_classify` already names the channel; reuse its label rather than
|
|
134
|
+
// re-deriving one, so the hook's output and the authoring-time
|
|
135
|
+
// linter's speak the same vocabulary.
|
|
136
|
+
add(typeof classified === 'string' ? classified : 'hidden-instruction', cp);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (_isStrippableControl(cp)) {
|
|
140
|
+
add('control-char', cp);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (_INVISIBLE_FILLERS.has(cp)) {
|
|
144
|
+
add('invisible-filler', cp);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
88
150
|
// ---------------------------------------------------------------------------
|
|
89
151
|
// Visible-layer scanner — FLAG ONLY, never rewrites
|
|
90
152
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One structural line per review run — the review surface's only telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Why it exists: before this file the review surface emitted nothing. Nothing
|
|
5
|
+
* recorded how often a review ran, which judges fired, or whether the spec
|
|
6
|
+
* axis ever changed a recommendation — so the value of adding a sixth judge
|
|
7
|
+
* was unmeasurable in either direction, and the cheapest response to a slower
|
|
8
|
+
* default path is to switch the new judge off. A number is the only defence
|
|
9
|
+
* against that, and the honest number includes "it changed nothing".
|
|
10
|
+
*
|
|
11
|
+
* PII-exclusion by CONSTRUCTION, not by scrubbing. `ReviewLine` has no field
|
|
12
|
+
* able to hold free-form content: every member is an enum, a boolean, a
|
|
13
|
+
* non-negative integer, or an ISO timestamp. There is no `payload`, `notes`,
|
|
14
|
+
* `extra`, `prompt`, `diff` or `path` member, and `buildReviewLine` REJECTS an
|
|
15
|
+
* input object carrying one rather than dropping it silently — a scrubber can
|
|
16
|
+
* fail, an absent field cannot. Same principle the artifact-engagement event
|
|
17
|
+
* and `orchestration_record`'s hex-only `payload_hash` apply.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately NOT reused from `_lib/orchestration_record.ts`: that line
|
|
20
|
+
* describes a subagent DISPATCH (tier, route, token deltas, cache hits). A
|
|
21
|
+
* review is not a dispatch, and widening `RecordInput` with review-shaped
|
|
22
|
+
* optionals would make both types describe neither thing precisely.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Was the spec axis able to run at all, and on what basis? */
|
|
26
|
+
export type SpecAxisReach = 'reachable_with_criteria' | 'reachable_no_criteria' | 'unreachable';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Did the spec axis change the recommendation the craft judges would have
|
|
30
|
+
* produced on their own?
|
|
31
|
+
*
|
|
32
|
+
* `null` is NOT "no" — it means the comparison never happened (the axis did
|
|
33
|
+
* not run, so there is no counterfactual). A `false` here would claim a
|
|
34
|
+
* comparison that was never made, which is the mis-attribution this field
|
|
35
|
+
* exists to expose. Same reasoning as `modelDivergent` in
|
|
36
|
+
* `_lib/orchestration_record.ts`.
|
|
37
|
+
*/
|
|
38
|
+
export type SpecAxisEffect = 'changed' | 'unchanged' | null;
|
|
39
|
+
|
|
40
|
+
/** The recommendation word the synthesis emitted. */
|
|
41
|
+
export type ReviewRecommendation = 'block' | 'revise' | 'proceed';
|
|
42
|
+
|
|
43
|
+
/** The criteria-source state, mirroring `judge-spec-compliance`'s three states. */
|
|
44
|
+
export type CriteriaSource = 'supplied' | 'not_provided' | 'supplied_unparseable';
|
|
45
|
+
|
|
46
|
+
/** The closed field set. A member not on this list cannot be written. */
|
|
47
|
+
export const REVIEW_LINE_FIELDS = [
|
|
48
|
+
'schema',
|
|
49
|
+
'at',
|
|
50
|
+
'judges_declared',
|
|
51
|
+
'judges_ran',
|
|
52
|
+
'spec_axis_reach',
|
|
53
|
+
'criteria_source',
|
|
54
|
+
'criteria_count',
|
|
55
|
+
'spec_missing',
|
|
56
|
+
'spec_partial',
|
|
57
|
+
'recommendation',
|
|
58
|
+
'spec_axis_effect',
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export type ReviewLineField = (typeof REVIEW_LINE_FIELDS)[number];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Field names that must never appear on this line — the free-form shapes a
|
|
65
|
+
* reviewer would reach for first. Checked as an INPUT rejection, so adding one
|
|
66
|
+
* fails loudly at the call site instead of being stripped downstream.
|
|
67
|
+
*/
|
|
68
|
+
export const FORBIDDEN_FREEFORM_FIELDS = [
|
|
69
|
+
'payload',
|
|
70
|
+
'notes',
|
|
71
|
+
'extra',
|
|
72
|
+
'prompt',
|
|
73
|
+
'diff',
|
|
74
|
+
'path',
|
|
75
|
+
'file',
|
|
76
|
+
'criteria',
|
|
77
|
+
'findings',
|
|
78
|
+
'message',
|
|
79
|
+
'body',
|
|
80
|
+
'content',
|
|
81
|
+
'text',
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
export interface ReviewLine {
|
|
85
|
+
readonly schema: 'review-axis-v1';
|
|
86
|
+
/** ISO-8601 UTC. Caller-supplied so the line is reproducible in a test. */
|
|
87
|
+
readonly at: string;
|
|
88
|
+
readonly judges_declared: number;
|
|
89
|
+
readonly judges_ran: number;
|
|
90
|
+
readonly spec_axis_reach: SpecAxisReach;
|
|
91
|
+
readonly criteria_source: CriteriaSource;
|
|
92
|
+
readonly criteria_count: number;
|
|
93
|
+
readonly spec_missing: number;
|
|
94
|
+
readonly spec_partial: number;
|
|
95
|
+
readonly recommendation: ReviewRecommendation;
|
|
96
|
+
readonly spec_axis_effect: SpecAxisEffect;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ReviewLineInput {
|
|
100
|
+
at: string;
|
|
101
|
+
judges_declared: number;
|
|
102
|
+
judges_ran: number;
|
|
103
|
+
spec_axis_reach: SpecAxisReach;
|
|
104
|
+
criteria_source: CriteriaSource;
|
|
105
|
+
criteria_count: number;
|
|
106
|
+
spec_missing: number;
|
|
107
|
+
spec_partial: number;
|
|
108
|
+
recommendation: ReviewRecommendation;
|
|
109
|
+
spec_axis_effect?: SpecAxisEffect;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface BuiltReviewLine {
|
|
113
|
+
line: ReviewLine | null;
|
|
114
|
+
errors: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
|
|
118
|
+
const REACH: readonly SpecAxisReach[] = ['reachable_with_criteria', 'reachable_no_criteria', 'unreachable'];
|
|
119
|
+
const SOURCE: readonly CriteriaSource[] = ['supplied', 'not_provided', 'supplied_unparseable'];
|
|
120
|
+
const RECO: readonly ReviewRecommendation[] = ['block', 'revise', 'proceed'];
|
|
121
|
+
|
|
122
|
+
function isCount(n: unknown): n is number {
|
|
123
|
+
return typeof n === 'number' && Number.isInteger(n) && n >= 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build + validate one `review-axis-v1` line.
|
|
128
|
+
*
|
|
129
|
+
* Returns `{line, errors}`; when `errors` is non-empty, `line` is null and the
|
|
130
|
+
* caller must NOT write. Never throws, never partially writes, never silently
|
|
131
|
+
* repairs an invalid input — the same contract as `buildOrchestrationLine`.
|
|
132
|
+
*/
|
|
133
|
+
export function buildReviewLine(input: ReviewLineInput): BuiltReviewLine {
|
|
134
|
+
const errors: string[] = [];
|
|
135
|
+
const raw = input as unknown as Record<string, unknown>;
|
|
136
|
+
|
|
137
|
+
for (const banned of FORBIDDEN_FREEFORM_FIELDS) {
|
|
138
|
+
if (Object.prototype.hasOwnProperty.call(raw, banned)) {
|
|
139
|
+
errors.push(
|
|
140
|
+
`'${banned}' is a free-form field and review-axis-v1 carries none — ` +
|
|
141
|
+
'privacy here is a property of the schema shape, not of a scrubbing pass',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof input.at !== 'string' || !ISO_RE.test(input.at)) {
|
|
147
|
+
errors.push('at must be an ISO-8601 UTC timestamp (…Z)');
|
|
148
|
+
}
|
|
149
|
+
for (const k of ['judges_declared', 'judges_ran', 'criteria_count', 'spec_missing', 'spec_partial'] as const) {
|
|
150
|
+
if (!isCount(input[k])) errors.push(`${k} must be a non-negative integer`);
|
|
151
|
+
}
|
|
152
|
+
if (isCount(input.judges_ran) && isCount(input.judges_declared) && input.judges_ran > input.judges_declared) {
|
|
153
|
+
errors.push('judges_ran cannot exceed judges_declared');
|
|
154
|
+
}
|
|
155
|
+
if (!REACH.includes(input.spec_axis_reach)) errors.push(`spec_axis_reach must be one of ${REACH.join(' | ')}`);
|
|
156
|
+
if (!SOURCE.includes(input.criteria_source)) errors.push(`criteria_source must be one of ${SOURCE.join(' | ')}`);
|
|
157
|
+
if (!RECO.includes(input.recommendation)) errors.push(`recommendation must be one of ${RECO.join(' | ')}`);
|
|
158
|
+
|
|
159
|
+
const effect = input.spec_axis_effect ?? null;
|
|
160
|
+
if (effect !== null && effect !== 'changed' && effect !== 'unchanged') {
|
|
161
|
+
errors.push('spec_axis_effect must be changed | unchanged | null');
|
|
162
|
+
}
|
|
163
|
+
// A counterfactual that never ran is `null`, never `unchanged`.
|
|
164
|
+
if (input.spec_axis_reach === 'unreachable' && effect !== null) {
|
|
165
|
+
errors.push('spec_axis_effect must be null when the axis was unreachable — there was no comparison to report');
|
|
166
|
+
}
|
|
167
|
+
// The two honest pairings of reach and source.
|
|
168
|
+
if (input.spec_axis_reach === 'reachable_with_criteria' && input.criteria_source !== 'supplied') {
|
|
169
|
+
errors.push('reachable_with_criteria requires criteria_source=supplied');
|
|
170
|
+
}
|
|
171
|
+
if (input.spec_axis_reach === 'reachable_no_criteria' && input.criteria_source === 'supplied') {
|
|
172
|
+
errors.push('reachable_no_criteria contradicts criteria_source=supplied');
|
|
173
|
+
}
|
|
174
|
+
if (input.criteria_source !== 'supplied' && isCount(input.criteria_count) && input.criteria_count > 0) {
|
|
175
|
+
errors.push('criteria_count must be 0 unless criteria were supplied');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (errors.length > 0) return { line: null, errors };
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
line: {
|
|
182
|
+
schema: 'review-axis-v1',
|
|
183
|
+
at: input.at,
|
|
184
|
+
judges_declared: input.judges_declared,
|
|
185
|
+
judges_ran: input.judges_ran,
|
|
186
|
+
spec_axis_reach: input.spec_axis_reach,
|
|
187
|
+
criteria_source: input.criteria_source,
|
|
188
|
+
criteria_count: input.criteria_count,
|
|
189
|
+
spec_missing: input.spec_missing,
|
|
190
|
+
spec_partial: input.spec_partial,
|
|
191
|
+
recommendation: input.recommendation,
|
|
192
|
+
spec_axis_effect: effect,
|
|
193
|
+
},
|
|
194
|
+
errors: [],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -61,10 +61,102 @@ export interface SubagentResponse {
|
|
|
61
61
|
* the same claim as "no assumptions were made".
|
|
62
62
|
*/
|
|
63
63
|
assumptions?: CapsuleAssumption[];
|
|
64
|
+
/**
|
|
65
|
+
* Where the tests in this change were authored FROM.
|
|
66
|
+
*
|
|
67
|
+
* `from-spec` — written against the acceptance criteria, before the diff
|
|
68
|
+
* existed. `from-diff` — written after reading the implementation.
|
|
69
|
+
* `unknown` — not recorded.
|
|
70
|
+
*
|
|
71
|
+
* **`unknown` is the default and an ABSENT field resolves to it, never to
|
|
72
|
+
* `from-spec`.** That asymmetry is the whole point of the field. The claim
|
|
73
|
+
* this metric exists to keep measurable is that a suite written by the same
|
|
74
|
+
* context as the implementation inherits its blind spots — so the valuable
|
|
75
|
+
* state is `from-spec`, and a default that let absence read as the valuable
|
|
76
|
+
* state would report the claim as satisfied by silence.
|
|
77
|
+
*
|
|
78
|
+
* Structural enum only: three string values and nothing that can hold
|
|
79
|
+
* free-form content. Same exclusion-by-construction the rest of this
|
|
80
|
+
* envelope uses — a field that cannot carry a prompt or a diff has no
|
|
81
|
+
* scrubber to fail.
|
|
82
|
+
*
|
|
83
|
+
* OPTIONAL, and deliberately not in `RESPONSE_REQUIRED_FIELDS`: that list
|
|
84
|
+
* is calibrated against a recorded ledger equivalence (`error_count: 5`),
|
|
85
|
+
* and adding a sixth required field would break it. A metric is not worth
|
|
86
|
+
* invalidating a measurement for.
|
|
87
|
+
*/
|
|
88
|
+
test_authorship?: TestAuthorship;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Three states. `unknown` is one of them, not the absence of the other two.
|
|
93
|
+
*
|
|
94
|
+
* Ordered by what they license rather than alphabetically: `from-spec` is the
|
|
95
|
+
* only value that supports the independence claim, `from-diff` is the weaker
|
|
96
|
+
* form the claim says is worthless, and `unknown` licenses nothing.
|
|
97
|
+
*/
|
|
98
|
+
export type TestAuthorship = 'from-spec' | 'from-diff' | 'unknown';
|
|
99
|
+
|
|
100
|
+
export const TEST_AUTHORSHIP: ReadonlySet<string> = new Set<TestAuthorship>([
|
|
101
|
+
'from-spec',
|
|
102
|
+
'from-diff',
|
|
103
|
+
'unknown',
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the field from a decoded envelope. ABSENT and UNRECOGNISED both
|
|
108
|
+
* resolve to `unknown`.
|
|
109
|
+
*
|
|
110
|
+
* An unrecognised value resolving to `unknown` rather than throwing is
|
|
111
|
+
* deliberate: this is a metric, and a producer that emits `from_spec` with an
|
|
112
|
+
* underscore should show up in the distribution as unrecorded, not stop a
|
|
113
|
+
* dispatch. The report is what surfaces it — a spike of `unknown` IS the
|
|
114
|
+
* finding that the field is not reaching its producers.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveTestAuthorship(input: unknown): TestAuthorship {
|
|
117
|
+
if (input === null || typeof input !== 'object') return 'unknown';
|
|
118
|
+
const v = (input as Record<string, unknown>)['test_authorship'];
|
|
119
|
+
return typeof v === 'string' && TEST_AUTHORSHIP.has(v) ? (v as TestAuthorship) : 'unknown';
|
|
64
120
|
}
|
|
65
121
|
|
|
66
122
|
const CONFIDENCE: ReadonlySet<string> = new Set<Confidence>(['low', 'medium', 'high']);
|
|
67
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The five fields `validateResponse` REQUIRES. Exported so the ledger's
|
|
126
|
+
* classifier counts contract fields against the validator's own list rather
|
|
127
|
+
* than a second copy that can drift from it.
|
|
128
|
+
*
|
|
129
|
+
* The optional fields (`artifact_paths`, `assumptions`) are deliberately NOT
|
|
130
|
+
* here. The boundary this list defines is "did the author aim at the envelope
|
|
131
|
+
* at all", and it is calibrated against a recorded measurement: every `fail`
|
|
132
|
+
* in the live ledger carries `error_count: 5`, which is exactly the count of
|
|
133
|
+
* required-field errors an object with none of them produces. Counting an
|
|
134
|
+
* optional field as a hit would break that equivalence, so an object carrying
|
|
135
|
+
* only `assumptions` reads as foreign — recorded here as a known edge, not as
|
|
136
|
+
* an oversight.
|
|
137
|
+
*/
|
|
138
|
+
export const RESPONSE_REQUIRED_FIELDS: readonly string[] = [
|
|
139
|
+
'summary',
|
|
140
|
+
'handoff',
|
|
141
|
+
'confidence',
|
|
142
|
+
'findings',
|
|
143
|
+
'risks',
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* How many required contract fields are PRESENT on a decoded object.
|
|
148
|
+
*
|
|
149
|
+
* Presence, never validity: a `summary: 123` is a contract attempt with a
|
|
150
|
+
* wrong type, which is a different defect from a fenced tool call that never
|
|
151
|
+
* aimed at the envelope. That distinction is the whole point of the count —
|
|
152
|
+
* `validateResponse` already reports the type errors.
|
|
153
|
+
*/
|
|
154
|
+
export function countContractFields(input: unknown): number {
|
|
155
|
+
if (input === null || typeof input !== 'object') return 0;
|
|
156
|
+
const r = input as Record<string, unknown>;
|
|
157
|
+
return RESPONSE_REQUIRED_FIELDS.filter((k) => Object.hasOwn(r, k)).length;
|
|
158
|
+
}
|
|
159
|
+
|
|
68
160
|
export interface ValidationResult {
|
|
69
161
|
valid: boolean;
|
|
70
162
|
errors: string[];
|
|
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import process from 'node:process';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
-
import { readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
|
|
19
|
+
import { readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
|
|
20
20
|
|
|
21
21
|
// ^ADR-(\d{3})-([a-z0-9-]+)\.md$
|
|
22
22
|
const NAMED = /^ADR-(\d{3})-([a-z0-9-]+)\.md$/;
|
|
@@ -29,8 +29,8 @@ const NAMED = /^ADR-(\d{3})-([a-z0-9-]+)\.md$/;
|
|
|
29
29
|
* amendments from one never touched.
|
|
30
30
|
*/
|
|
31
31
|
const HEAD =
|
|
32
|
-
'| # | Title | Status | Date | Supersedes | Superseded by | Amended by |\n' +
|
|
33
|
-
'
|
|
32
|
+
'| # | Title | Status | Date | Provenance | Evidence | Supersedes | Superseded by | Amended by |\n' +
|
|
33
|
+
'|---|---|---|---|---|---|---|---|---|';
|
|
34
34
|
|
|
35
35
|
type Meta = Record<string, string>;
|
|
36
36
|
interface Row extends Meta {
|
|
@@ -130,10 +130,16 @@ export function scan(d: string): [Row[], Row[], string[]] {
|
|
|
130
130
|
if (name === 'INDEX.md') {
|
|
131
131
|
continue;
|
|
132
132
|
}
|
|
133
|
-
const
|
|
133
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
134
|
+
const meta = fm(text);
|
|
135
|
+
// Read apart from `meta` on purpose. `provenance` and `evidence` are
|
|
136
|
+
// nested, so the scalar reader keeps them out of its map entirely —
|
|
137
|
+
// spreading `meta` could never produce these two cells, and the axis
|
|
138
|
+
// reader is where the `discovery`-rides-in-the-grade rule lives.
|
|
139
|
+
const axes = readAdrAxisCells(text);
|
|
134
140
|
const m = NAMED.exec(name);
|
|
135
141
|
if (!m) {
|
|
136
|
-
leg.push({ path: name, ...meta });
|
|
142
|
+
leg.push({ path: name, ...meta, ...axes });
|
|
137
143
|
continue;
|
|
138
144
|
}
|
|
139
145
|
const n = m[1] as string;
|
|
@@ -144,7 +150,7 @@ export function scan(d: string): [Row[], Row[], string[]] {
|
|
|
144
150
|
errs.push(`ADR-${n} duplicate: ${name} and ${seen[n]}`);
|
|
145
151
|
}
|
|
146
152
|
seen[n] = name;
|
|
147
|
-
num.push({ num: n, slug: m[2] as string, path: name, ...meta });
|
|
153
|
+
num.push({ num: n, slug: m[2] as string, path: name, ...meta, ...axes });
|
|
148
154
|
}
|
|
149
155
|
const nums = new Set(num.map((r) => r.num as string));
|
|
150
156
|
for (const r of num) {
|
|
@@ -172,7 +178,8 @@ export function row(r: Row): string {
|
|
|
172
178
|
const label = 'num' in r ? `ADR-${r.num}` : (r.path as string).slice(0, -3);
|
|
173
179
|
return (
|
|
174
180
|
`| [${label}](${r.path}) | ${title} | ${r.status ?? '—'} ` +
|
|
175
|
-
`| ${r.date ?? '—'} | ${r.
|
|
181
|
+
`| ${r.date ?? '—'} | ${r.provenance_kind ?? '—'} | ${r.evidence_grade ?? '—'} ` +
|
|
182
|
+
`| ${r.supersedes ?? '—'} | ${r.superseded_by ?? '—'} ` +
|
|
176
183
|
`| ${r.amended_by ?? '—'} |`
|
|
177
184
|
);
|
|
178
185
|
}
|
|
@@ -148,7 +148,18 @@ export const CODEX_MEASURED_UNSERVABLE: ReadonlyMap<string, string> = new Map([
|
|
|
148
148
|
]);
|
|
149
149
|
|
|
150
150
|
export const DEFAULT_OPENAI_CLI_MODEL = OPENAI_CLI_VENDOR_DEFAULT;
|
|
151
|
-
|
|
151
|
+
// A vendor ALIAS, not a version. `claude --help` documents 'fable', 'opus' and
|
|
152
|
+
// 'sonnet' as "an alias for the latest model" in their band, so an alias cannot
|
|
153
|
+
// go stale the way a dated id can — which is the whole point of the change that
|
|
154
|
+
// put it here.
|
|
155
|
+
//
|
|
156
|
+
// It was `'claude-sonnet-4-5'`, and this line is the half the roadmap that
|
|
157
|
+
// found the defect did not know about: refreshing only the template's pin would
|
|
158
|
+
// have left every member that omits `model:` falling back to the SAME stale id
|
|
159
|
+
// from code. Read from the provider's own CLI surface on 2026-08-22, never from
|
|
160
|
+
// the agent's recall — the source that road-to-council-seat-selection's own
|
|
161
|
+
// Risk 1 declares inadmissible.
|
|
162
|
+
export const DEFAULT_ANTHROPIC_CLI_MODEL = 'sonnet';
|
|
152
163
|
export const DEFAULT_GEMINI_CLI_MODEL = 'gemini-2.5-pro';
|
|
153
164
|
|
|
154
165
|
// OpenAI-API-compatible endpoints. xAI and Perplexity both expose the
|