@event4u/agent-config 13.0.0 → 14.0.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 +160 -333
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
- package/dist/agent-src/rules/ui-audit-gate.md +25 -8
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
- package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +40 -18
- 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 +2018 -1320
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1652 -892
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +111 -5
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +20 -0
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +80 -143
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/capture_skill_catalogue.ts +264 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/hook_manifest.yaml +72 -5
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/release.ts +38 -53
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Rule-activation census — which rules the Claude host projection scopes, and why.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-mixed-trigger-activation-cost` step 1.0. It exists because the field
|
|
6
|
+
* check that roadmap inherited from its source analysis is **refuted as a
|
|
7
|
+
* discriminator**:
|
|
8
|
+
*
|
|
9
|
+
* grep -l '^paths:' .claude/rules/*.md | wc -l
|
|
10
|
+
*
|
|
11
|
+
* The source predicted ~25 for a pre-guard projection and ~6 for a post-guard
|
|
12
|
+
* one, so a colleague could decide in one minute whether the mixed-trigger flip
|
|
13
|
+
* was live on their machine. Run on a maintainer checkout it returns **0** —
|
|
14
|
+
* neither number — because `.claude/rules/` is a generated, gitignored artefact
|
|
15
|
+
* whose scope and freshness are unknown at read time. A reading of 0 is
|
|
16
|
+
* consistent with "the flip is live", with "this projection was generated at a
|
|
17
|
+
* different `--scope`", and with "this projection is simply stale", and the
|
|
18
|
+
* grep cannot tell those apart. A probe whose three possible causes are
|
|
19
|
+
* indistinguishable is not a probe.
|
|
20
|
+
*
|
|
21
|
+
* This census reads the **source** instead, and reads it through the emitter's
|
|
22
|
+
* own exported functions rather than a reimplementation: `_parse_frontmatter`
|
|
23
|
+
* for the frontmatter, `_has_non_path_trigger` for the mixed test, and
|
|
24
|
+
* `_claude_paths_plan` for the verdict. So "would this rule be scoped" is
|
|
25
|
+
* answered by the code that actually decides it — the method the source
|
|
26
|
+
* analysis used across two tags, applied to one tree.
|
|
27
|
+
*
|
|
28
|
+
* `_parse_frontmatter` is exported from `condense.ts` for exactly this, and it is
|
|
29
|
+
* the reason the export carries only a one-line pointer there: `condense.ts` sits
|
|
30
|
+
* far above the 1,500-line source ceiling, so every line added to it counts
|
|
31
|
+
* against the `check_source_size_budget` ratchet. The rationale lives here, in a
|
|
32
|
+
* file the ratchet does not charge, rather than in the file it does.
|
|
33
|
+
* A reimplemented parse is the divergence class that made the shipped
|
|
34
|
+
* `.claude/rules/` count unreadable as evidence in the first place.
|
|
35
|
+
*
|
|
36
|
+
* What it does NOT do, stated because the distinction is the whole point: it
|
|
37
|
+
* does not tell you what a given machine's `.claude/rules/` currently holds.
|
|
38
|
+
* That is a property of an install, not of the repo. `--projection` reads the
|
|
39
|
+
* host projection alongside the source and reports the two side by side,
|
|
40
|
+
* labelled, so a divergence is visible as a divergence instead of being averaged
|
|
41
|
+
* into one misleading number.
|
|
42
|
+
*
|
|
43
|
+
* CLI contract: exit 0 always on a successful scan — this is an instrument, not
|
|
44
|
+
* a gate. `--json` emits the machine-readable record, `--projection <dir>` adds
|
|
45
|
+
* the host-projection column, `--quiet` suppresses the human table.
|
|
46
|
+
* The gate that ratchets these numbers is `check_rule_activation_census.ts`
|
|
47
|
+
* (step 4.1); keeping measurement and enforcement in separate files is why this
|
|
48
|
+
* one has no failure exit.
|
|
49
|
+
*
|
|
50
|
+
* The filename is deliberately NOT `check_`/`lint_`-prefixed. That prefix set is
|
|
51
|
+
* what `_lib/gate_population.ts` treats as gate-shaped, and a file in the gate
|
|
52
|
+
* population owes the ledger a per-target accounting — which an instrument that
|
|
53
|
+
* cannot fail has nothing honest to report. The name carries the distinction so
|
|
54
|
+
* no `// ledger-exempt:` marker is needed here; a marker would assert an
|
|
55
|
+
* exemption from an obligation this file never incurs.
|
|
56
|
+
*/
|
|
57
|
+
import * as fs from 'node:fs';
|
|
58
|
+
import * as path from 'node:path';
|
|
59
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
60
|
+
|
|
61
|
+
import { _claude_paths_plan, _has_non_path_trigger, _parse_frontmatter } from './condense.js';
|
|
62
|
+
|
|
63
|
+
const RULES_DIR = path.join('src', 'rules');
|
|
64
|
+
|
|
65
|
+
/** Trigger keys that describe a file path rather than prompt text. */
|
|
66
|
+
const PATH_TRIGGER_KEYS = ['file_pattern', 'path_prefix'] as const;
|
|
67
|
+
|
|
68
|
+
/** Trigger keys that describe prompt text rather than a file path. */
|
|
69
|
+
const TEXT_TRIGGER_KEYS = ['keyword', 'phrase'] as const;
|
|
70
|
+
|
|
71
|
+
export interface RuleActivation {
|
|
72
|
+
/** Rule id — the basename without `.md`. */
|
|
73
|
+
id: string;
|
|
74
|
+
/** `always` / `auto` / `manual`, or `unknown` when the field is absent. */
|
|
75
|
+
type: string;
|
|
76
|
+
/** Declares at least one `file_pattern` or `path_prefix` trigger. */
|
|
77
|
+
has_path_trigger: boolean;
|
|
78
|
+
/** Declares at least one `keyword` or `phrase` trigger. */
|
|
79
|
+
has_text_trigger: boolean;
|
|
80
|
+
/** Both of the above — the class the mixed-triggers guard governs. */
|
|
81
|
+
mixed: boolean;
|
|
82
|
+
/** Globs the emitter would write under `paths:`. Empty means unconditional. */
|
|
83
|
+
emitted_globs: string[];
|
|
84
|
+
/** Patterns the emitter deliberately drops, with the reason it records. */
|
|
85
|
+
dropped: { pattern: string; reason: string }[];
|
|
86
|
+
/**
|
|
87
|
+
* The emitter's verdict in one word.
|
|
88
|
+
* `scoped` — loads on a path match · `unconditional` — loads every session ·
|
|
89
|
+
* `always` — an Iron-Law rule, unconditional by design and never a candidate.
|
|
90
|
+
*/
|
|
91
|
+
verdict: 'scoped' | 'unconditional' | 'always';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _trigger_list(meta: Record<string, unknown>): Record<string, unknown>[] {
|
|
95
|
+
const raw = meta['triggers'];
|
|
96
|
+
if (!Array.isArray(raw)) return [];
|
|
97
|
+
return raw.filter(
|
|
98
|
+
(t): t is Record<string, unknown> =>
|
|
99
|
+
t !== null && typeof t === 'object' && !Array.isArray(t),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _declares(meta: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
104
|
+
for (const t of _trigger_list(meta)) {
|
|
105
|
+
for (const k of keys) {
|
|
106
|
+
const v = t[k];
|
|
107
|
+
if (typeof v === 'string' && v) return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Classify one rule file through the emitter's own exported decision path. */
|
|
114
|
+
export function classify_rule(id: string, content: string): RuleActivation {
|
|
115
|
+
const [meta] = _parse_frontmatter(content);
|
|
116
|
+
const plan = _claude_paths_plan(meta);
|
|
117
|
+
const has_path_trigger = _declares(meta, PATH_TRIGGER_KEYS);
|
|
118
|
+
const has_text_trigger = _declares(meta, TEXT_TRIGGER_KEYS);
|
|
119
|
+
const type = typeof meta['type'] === 'string' ? (meta['type'] as string) : 'unknown';
|
|
120
|
+
const always = Boolean(meta['alwaysApply'] || type === 'always');
|
|
121
|
+
|
|
122
|
+
// `_has_non_path_trigger` is the guard's own predicate and is deliberately
|
|
123
|
+
// narrower than `has_text_trigger` looks: it ignores `command:` triggers.
|
|
124
|
+
// Reported separately rather than conflated, because a rule that is mixed by
|
|
125
|
+
// the guard's definition is the one that flipped.
|
|
126
|
+
const mixed = has_path_trigger && _has_non_path_trigger(meta);
|
|
127
|
+
|
|
128
|
+
let verdict: RuleActivation['verdict'];
|
|
129
|
+
if (always) verdict = 'always';
|
|
130
|
+
else if (plan.globs.length > 0) verdict = 'scoped';
|
|
131
|
+
else verdict = 'unconditional';
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id,
|
|
135
|
+
type,
|
|
136
|
+
has_path_trigger,
|
|
137
|
+
has_text_trigger,
|
|
138
|
+
mixed,
|
|
139
|
+
emitted_globs: plan.globs,
|
|
140
|
+
dropped: plan.dropped.map((d) => ({ pattern: d.pattern, reason: d.reason })),
|
|
141
|
+
verdict,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function census(root: string): RuleActivation[] {
|
|
146
|
+
const dir = path.join(root, RULES_DIR);
|
|
147
|
+
const files = fs
|
|
148
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
149
|
+
.filter((e) => !e.isDirectory() && e.name.endsWith('.md'))
|
|
150
|
+
.map((e) => e.name)
|
|
151
|
+
.sort();
|
|
152
|
+
return files.map((f) =>
|
|
153
|
+
classify_rule(f.replace(/\.md$/, ''), fs.readFileSync(path.join(dir, f), 'utf8')),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read a host projection's own `paths:` count.
|
|
159
|
+
*
|
|
160
|
+
* Reported beside the source census, never merged into it. The two answer
|
|
161
|
+
* different questions — "what would the emitter do with this tree" versus "what
|
|
162
|
+
* does this directory currently contain" — and the second carries no record of
|
|
163
|
+
* the scope or the commit it was generated at, which is exactly why the grep it
|
|
164
|
+
* replaces could not be read as evidence.
|
|
165
|
+
*/
|
|
166
|
+
export function projection_reading(dir: string): { files: number; with_paths: number } | null {
|
|
167
|
+
if (!fs.existsSync(dir)) return null;
|
|
168
|
+
const files = fs
|
|
169
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
170
|
+
.filter((e) => !e.isDirectory() && e.name.endsWith('.md'))
|
|
171
|
+
.map((e) => e.name);
|
|
172
|
+
let with_paths = 0;
|
|
173
|
+
for (const f of files) {
|
|
174
|
+
const text = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
175
|
+
if (/^paths:/m.test(text)) with_paths += 1;
|
|
176
|
+
}
|
|
177
|
+
return { files: files.length, with_paths };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface CensusSummary {
|
|
181
|
+
total: number;
|
|
182
|
+
path_shaped: number;
|
|
183
|
+
mixed: number;
|
|
184
|
+
path_only: number;
|
|
185
|
+
scoped: number;
|
|
186
|
+
unconditional: number;
|
|
187
|
+
always: number;
|
|
188
|
+
mixed_ids: string[];
|
|
189
|
+
path_only_ids: string[];
|
|
190
|
+
scoped_ids: string[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function summarize(rows: RuleActivation[]): CensusSummary {
|
|
194
|
+
const path_shaped = rows.filter((r) => r.has_path_trigger);
|
|
195
|
+
const mixed = path_shaped.filter((r) => r.mixed);
|
|
196
|
+
const path_only = path_shaped.filter((r) => !r.mixed);
|
|
197
|
+
return {
|
|
198
|
+
total: rows.length,
|
|
199
|
+
path_shaped: path_shaped.length,
|
|
200
|
+
mixed: mixed.length,
|
|
201
|
+
path_only: path_only.length,
|
|
202
|
+
scoped: rows.filter((r) => r.verdict === 'scoped').length,
|
|
203
|
+
unconditional: rows.filter((r) => r.verdict === 'unconditional').length,
|
|
204
|
+
always: rows.filter((r) => r.verdict === 'always').length,
|
|
205
|
+
mixed_ids: mixed.map((r) => r.id),
|
|
206
|
+
path_only_ids: path_only.map((r) => r.id),
|
|
207
|
+
scoped_ids: rows.filter((r) => r.verdict === 'scoped').map((r) => r.id),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _arg(flag: string): string | null {
|
|
212
|
+
const argv = process.argv.slice(2);
|
|
213
|
+
const i = argv.indexOf(flag);
|
|
214
|
+
return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function main(): number {
|
|
218
|
+
const argv = process.argv.slice(2);
|
|
219
|
+
const as_json = argv.includes('--json');
|
|
220
|
+
const quiet = argv.includes('--quiet');
|
|
221
|
+
const root = _arg('--root') ?? process.cwd();
|
|
222
|
+
const projection_dir = _arg('--projection');
|
|
223
|
+
|
|
224
|
+
const rows = census(root);
|
|
225
|
+
const sum = summarize(rows);
|
|
226
|
+
const projection = projection_dir
|
|
227
|
+
? projection_reading(path.isAbsolute(projection_dir)
|
|
228
|
+
? projection_dir
|
|
229
|
+
: path.join(root, projection_dir))
|
|
230
|
+
: null;
|
|
231
|
+
|
|
232
|
+
if (as_json) {
|
|
233
|
+
process.stdout.write(
|
|
234
|
+
`${JSON.stringify({ summary: sum, projection, rules: rows }, null, 2)}\n`,
|
|
235
|
+
);
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
if (quiet) return 0;
|
|
239
|
+
|
|
240
|
+
process.stdout.write('Rule-activation census — source of truth is src/rules/\n\n');
|
|
241
|
+
process.stdout.write(` rule files ${sum.total}\n`);
|
|
242
|
+
process.stdout.write(` declaring a path-shaped trigger ${sum.path_shaped}\n`);
|
|
243
|
+
process.stdout.write(` …of those, MIXED (also keyword/phrase) ${sum.mixed}\n`);
|
|
244
|
+
process.stdout.write(` …of those, path-only ${sum.path_only}\n`);
|
|
245
|
+
process.stdout.write('\n emitter verdict:\n');
|
|
246
|
+
process.stdout.write(` scoped (loads on a path match) ${sum.scoped}\n`);
|
|
247
|
+
process.stdout.write(` unconditional (every session) ${sum.unconditional}\n`);
|
|
248
|
+
process.stdout.write(` always (Iron Law, by design) ${sum.always}\n`);
|
|
249
|
+
|
|
250
|
+
if (sum.mixed > 0) {
|
|
251
|
+
process.stdout.write(
|
|
252
|
+
`\n the mixed set — these load every session and are the flip's cost:\n`,
|
|
253
|
+
);
|
|
254
|
+
for (const id of sum.mixed_ids) process.stdout.write(` - ${id}\n`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (projection) {
|
|
258
|
+
process.stdout.write(
|
|
259
|
+
`\n host projection at ${projection_dir} — reported SEPARATELY, not merged:\n` +
|
|
260
|
+
` files ${projection.files} · declaring paths: ${projection.with_paths}\n`,
|
|
261
|
+
);
|
|
262
|
+
if (projection.with_paths !== sum.scoped) {
|
|
263
|
+
process.stdout.write(
|
|
264
|
+
` ⚠️ diverges from the source verdict (${sum.scoped} scoped). A projection\n` +
|
|
265
|
+
` carries no record of the scope or commit it was generated at, so this\n` +
|
|
266
|
+
` is a reason to regenerate it — never a reading of the emitter.\n`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} else if (projection_dir) {
|
|
270
|
+
// Three states, kept apart on purpose. Collapsing "you asked and the
|
|
271
|
+
// directory is not there" into "you did not ask" would reproduce, in this
|
|
272
|
+
// script's own output, the exact conflation it was written to end — and a
|
|
273
|
+
// fresh worktree has no `.claude/` at all, so this is the common case.
|
|
274
|
+
process.stdout.write(
|
|
275
|
+
`\n host projection at ${projection_dir}: DIRECTORY ABSENT.\n` +
|
|
276
|
+
' Not a reading of zero — there is nothing to read. A generated projection is\n' +
|
|
277
|
+
' absent in a fresh worktree until `task generate-tools` has run.\n',
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
process.stdout.write(
|
|
281
|
+
'\n no --projection given: this census describes the SOURCE only.\n' +
|
|
282
|
+
' It deliberately makes no claim about any machine\'s .claude/rules/.\n',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(fileURLToPath(import.meta.url)).href) {
|
|
289
|
+
const invoked = path.resolve(process.argv[1]);
|
|
290
|
+
const self = path.resolve(fileURLToPath(import.meta.url));
|
|
291
|
+
if (invoked === self) process.exit(main());
|
|
292
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "evaluator-output.schema.json",
|
|
4
|
+
"title": "Evaluator output",
|
|
5
|
+
"description": "ONE shape for a mechanical verifier's machine-readable verdict, so a loop that optimizes against a metric is decoupled from the verifier producing it. Measured before drafting (spike s02, agents/evidence/eval-findings/metric-loop-s02.md): three existing verifiers — validate_frontmatter, lint_output_slop, check_references — were wrapped in this shape with ZERO invasive changes to the verifiers, so the contract is transplantable onto the surface that exists rather than one that would have to be built. The error semantics live in docs/contracts/evaluator-output.md; without them every caller invents its own failure handling, which is the half that makes a contract usable.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"name",
|
|
10
|
+
"pass",
|
|
11
|
+
"score"
|
|
12
|
+
],
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"schema_version": {
|
|
16
|
+
"enum": [
|
|
17
|
+
1
|
|
18
|
+
],
|
|
19
|
+
"description": "Pinned. Spelled as a one-member `enum` rather than `const` for the same measured reason review-findings.schema.json records: this repo's Draft-07 subset validator enforces `enum` and silently ignores `const`, so a `const` here would be a pin nothing checks."
|
|
20
|
+
},
|
|
21
|
+
"name": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"maxLength": 120,
|
|
25
|
+
"description": "Stable identifier of the evaluator, not of the run. The loop register keys decisions on it, so renaming an evaluator starts a new metric series rather than continuing the old one — which is the honest behaviour, since the numbers are not comparable across a definition change."
|
|
26
|
+
},
|
|
27
|
+
"pass": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"description": "Did the underlying verifier accept the tree? This is the SOVEREIGN field: a keep-or-revert loop gates on `pass && score > baseline`, never on `score` alone. Measured in spike s01 — a change that improved the metric by 67 % and broke behaviour was reverted only because `pass` was false. A loop reading `score` alone keeps it."
|
|
30
|
+
},
|
|
31
|
+
"score": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "HIGHER IS BETTER, always. A verifier whose natural metric is a violation count negates it (`score = -violations`), so every consumer compares with one operator and no caller has to know the direction. `direction` records which way the raw metric ran, for humans reading the register; it is never the comparison input."
|
|
34
|
+
},
|
|
35
|
+
"metric": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "The raw, un-negated measurement. ABSENT — not null — when there is no number to report; `metric_state` then says why. Spelled as an optional `number` rather than `[\"number\", \"null\"]` for a measured reason, the same class as the `const`-vs-`enum` note above: this repo's Draft-07 subset validator rejects an array-typed `type` outright (`Unsupported schema type 'number,null'`), so a union here would have failed every conformant emitter. Spike s02 produced the distinction the field encodes: check_references writes its count to stderr, and a stdout-only reader reported no metric beside `pass: true` — a degraded reading indistinguishable from an evaluator that legitimately has no number."
|
|
38
|
+
},
|
|
39
|
+
"metric_state": {
|
|
40
|
+
"enum": [
|
|
41
|
+
"present",
|
|
42
|
+
"absent",
|
|
43
|
+
"unreadable"
|
|
44
|
+
],
|
|
45
|
+
"description": "Why `metric` holds what it holds. `present` — a number was read. `absent` — this evaluator has no scalar metric by design. `unreadable` — a metric was expected and could not be parsed; the run is DEGRADED and a loop must treat it as a failed experiment rather than as a zero. Defaulting `unreadable` to `absent` is the exact silent degradation this field exists to make impossible."
|
|
46
|
+
},
|
|
47
|
+
"direction": {
|
|
48
|
+
"enum": [
|
|
49
|
+
"maximize",
|
|
50
|
+
"minimize"
|
|
51
|
+
],
|
|
52
|
+
"description": "Which way the RAW metric runs. Documentation for a human reading the register, never a comparison input — `score` has already absorbed the direction. Recorded because a register row showing `metric: 3` is uninterpretable without it."
|
|
53
|
+
},
|
|
54
|
+
"error": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"maxLength": 2000,
|
|
57
|
+
"description": "Present only when the evaluator itself failed to produce a verdict (as opposed to the verifier reporting a red tree, which is `pass: false` with no error). The two are different events for a loop: a red tree is a result to act on, an evaluator failure is a measurement that did not happen."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -20,6 +20,37 @@
|
|
|
20
20
|
"type": "string",
|
|
21
21
|
"description": "Version the ledger belongs to. Present on the release-findings ledger (agents/evidence/release-findings/<version>.json); absent on a bare --findings-out emission, which is why it is not required here."
|
|
22
22
|
},
|
|
23
|
+
"review_independence": {
|
|
24
|
+
"enum": [
|
|
25
|
+
"cross-family",
|
|
26
|
+
"same-family",
|
|
27
|
+
"single-member",
|
|
28
|
+
"unknown"
|
|
29
|
+
],
|
|
30
|
+
"description": "Whether the reviewer set spans more than one model family. Added because the artifact was previously silent on it: the self-review gate calls ONE Anthropic client, and the ledger it wrote was byte-indistinguishable from one backed by independent models — so a consumer read 'no findings' as acceptance either way. `unknown` is an honest value for a set that was not recorded, NOT a synonym for cross-family."
|
|
31
|
+
},
|
|
32
|
+
"acceptance_status": {
|
|
33
|
+
"enum": [
|
|
34
|
+
"accepted",
|
|
35
|
+
"provisional"
|
|
36
|
+
],
|
|
37
|
+
"description": "What a consumer may claim this artifact establishes. DERIVED from `review_independence` (see _lib/review_independence.ts), never set independently: two fields a producer can set inconsistently reintroduce exactly the ambiguity the pair removes, and a same-family set carrying `accepted` would still read as acceptance to anyone scanning one field. Only `cross-family` yields `accepted`; `unknown` yields `provisional`, because an unrecorded member set is not evidence of independence."
|
|
38
|
+
},
|
|
39
|
+
"assurance": {
|
|
40
|
+
"enum": [
|
|
41
|
+
"unreviewed",
|
|
42
|
+
"single-pass",
|
|
43
|
+
"independent"
|
|
44
|
+
],
|
|
45
|
+
"description": "How much INDEPENDENT evidence backs the verdict — deliberately orthogonal to how much effort the review spent. Collapsing the two is the mechanism by which a long, careful, single-model pass reads as acceptance: effort is visible in the artifact (finding count, detail, rationale) and independence is not, so a reader with only one axis substitutes the one they can see. A five-hour same-family review is `single-pass`; a terse two-family one is `independent`."
|
|
46
|
+
},
|
|
47
|
+
"reviewers": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"description": "The member names the independence verdict was derived from, recorded so the derivation is auditable rather than asserted. Provider names as `ai_council/config.ts::_VALID_PROVIDERS` spells them.",
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
23
54
|
"findings": {
|
|
24
55
|
"type": "array",
|
|
25
56
|
"description": "May be empty. An empty array is an honest null, not a missing field — the distinction the Markdown track spells with its own §2.3 line. The item shape is INLINED rather than `$ref`-ed for the same measured reason: the subset validator does not resolve `$ref`, so a referenced definition would have validated nothing.",
|
|
@@ -35,6 +35,7 @@ import path from 'node:path';
|
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
|
|
37
37
|
import { AnthropicClient, load_anthropic_key } from './ai_council/clients.js';
|
|
38
|
+
import { independenceFields } from './_lib/review_independence.js';
|
|
38
39
|
|
|
39
40
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
40
41
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -458,11 +459,16 @@ export function main(argv: string[]): 0 | 2 {
|
|
|
458
459
|
if (outIdx >= 0 && argv[outIdx + 1]) {
|
|
459
460
|
// Durable ingestion input for the disposition ledger (release-truth
|
|
460
461
|
// Phase 3): `check_finding_dispositions --ingest <this file>`.
|
|
462
|
+
// The independence fields are set HERE, at write time, from the
|
|
463
|
+
// actual reviewer set — which for this gate is one Anthropic client,
|
|
464
|
+
// so the artifact declares itself single-member and provisional. It
|
|
465
|
+
// previously said nothing, and silence read as acceptance.
|
|
461
466
|
writeFileSync(
|
|
462
467
|
argv[outIdx + 1]!,
|
|
463
468
|
JSON.stringify(
|
|
464
469
|
{
|
|
465
470
|
schema_version: 1,
|
|
471
|
+
...independenceFields(['anthropic']),
|
|
466
472
|
findings: findings.map((f) => ({ finding_id: findingId(f), ...f })),
|
|
467
473
|
},
|
|
468
474
|
null,
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
ttl_seconds_for,
|
|
68
68
|
write_record,
|
|
69
69
|
} from './_lib/session_register.js';
|
|
70
|
+
import { pruneAgedRefusalState, readSessionCounts } from './_lib/turn_end_refusals.js';
|
|
70
71
|
import { readHookStdin } from './hooks/hook_stdin.js';
|
|
71
72
|
|
|
72
73
|
/** Replay-fixture runs must never mutate state (same contract as chat_history). */
|
|
@@ -323,7 +324,7 @@ export function build_record(
|
|
|
323
324
|
started_at: string,
|
|
324
325
|
now: Date = new Date(),
|
|
325
326
|
): SessionRecord {
|
|
326
|
-
|
|
327
|
+
const rec: SessionRecord = {
|
|
327
328
|
session_id,
|
|
328
329
|
platform,
|
|
329
330
|
worktree: workspace_root,
|
|
@@ -332,6 +333,20 @@ export function build_record(
|
|
|
332
333
|
started_at,
|
|
333
334
|
last_seen: iso_now(now),
|
|
334
335
|
};
|
|
336
|
+
// road-to-stop-gate-honesty step 1.1 — the per-session half of the refusal
|
|
337
|
+
// counter. Only ever ADDS a field: a session with no refusals leaves the
|
|
338
|
+
// record byte-identical to what it was before this existed, so a reader that
|
|
339
|
+
// does not know the field is unaffected and a diff of two records still
|
|
340
|
+
// shows only what changed.
|
|
341
|
+
const counts = readSessionCounts(workspace_root, session_id);
|
|
342
|
+
if (counts !== null) {
|
|
343
|
+
const nonZero: Record<string, number> = {};
|
|
344
|
+
for (const [id, n] of Object.entries(counts)) {
|
|
345
|
+
if (n > 0) nonZero[id] = n;
|
|
346
|
+
}
|
|
347
|
+
if (Object.keys(nonZero).length > 0) rec.turn_end_refusals = nonZero;
|
|
348
|
+
}
|
|
349
|
+
return rec;
|
|
335
350
|
}
|
|
336
351
|
|
|
337
352
|
/**
|
|
@@ -590,6 +605,20 @@ export function main(): number {
|
|
|
590
605
|
);
|
|
591
606
|
}
|
|
592
607
|
}
|
|
608
|
+
if (event === 'session_start') {
|
|
609
|
+
// road-to-stop-gate-honesty step 1.2 — the TTL the gate's own
|
|
610
|
+
// header admitted was missing. This concern is the carrier
|
|
611
|
+
// because it is already the session_start pruner: adding a
|
|
612
|
+
// second prune here costs one directory scan on a slot that
|
|
613
|
+
// fires once per session, where a new concern would cost a
|
|
614
|
+
// spawn. Never throws — evidence retention is not a reason to
|
|
615
|
+
// fail a session start.
|
|
616
|
+
try {
|
|
617
|
+
pruneAgedRefusalState(root, { now: new Date() });
|
|
618
|
+
} catch {
|
|
619
|
+
/* an unprunable directory keeps its files; it is not an error */
|
|
620
|
+
}
|
|
621
|
+
}
|
|
593
622
|
if (event === 'session_start') {
|
|
594
623
|
const block = foreign_sessions_block(root, session_id, new Date());
|
|
595
624
|
if (block !== null) {
|