@event4u/agent-config 9.29.0 → 9.31.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 +32 -214
- package/README.md +1 -0
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/delegate.md +1 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +4 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +11 -1
- package/dist/agent-src/rules/preservation-guard.md +9 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +10 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-RCIA67XN.js → chunk-2WCLVDWC.js} +45 -2
- package/dist/cli-delegate/{chunk-66OVWGB5.js → chunk-AG47LAGQ.js} +1 -1
- package/dist/cli-delegate/{chunk-MP3JT3IU.js → chunk-DS6BLDMG.js} +1 -1
- package/dist/cli-delegate/chunk-EJITJBJX.js +172 -0
- package/dist/cli-delegate/{chunk-NZ5N2XQL.js → chunk-ITIQSGOQ.js} +3 -3
- package/dist/cli-delegate/{chunk-LCFJIQ4J.js → chunk-JEJABGKB.js} +110 -6
- package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/{chunk-OJM3MBJM.js → chunk-QEEB4LHO.js} +22 -35
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- package/dist/cli-delegate/{chunk-MMXA5GY7.js → chunk-UVKNIJH2.js} +14 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +2861 -96
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +87 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +3 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +3 -56
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- 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 +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2143 -641
- package/dist/install/install.mjs +133 -14
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3967 -761
- package/dist/scripts/_lib/agent_settings.js +111 -2
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +21 -5
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/config/agent-settings.template.yml +36 -0
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/pack-size-budget.json +3 -2
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +17 -2
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_cli/explain_last/inputs.ts +13 -0
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/agent_settings.ts +119 -2
- package/src/scripts/_lib/carrier_divergence.ts +47 -0
- package/src/scripts/_lib/judgment_ladder.ts +254 -2
- package/src/scripts/_lib/orchestration_record.ts +41 -10
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +194 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +159 -0
- package/src/scripts/_lib/subagent_response.ts +75 -0
- package/src/scripts/ai_council/clients.ts +50 -1
- package/src/scripts/ask_transport.ts +221 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +17 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +20 -0
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +40 -3
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +715 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +143 -1
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +272 -0
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/rebuild_hook_bundle.ts +1 -0
- package/src/scripts/report_carrier_divergence.ts +72 -24
- package/src/scripts/report_conformance_funnel.ts +291 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +97 -0
- package/src/scripts/self_repair_cli.ts +51 -3
- package/src/scripts/session_eol_report.ts +261 -0
- package/src/server/schemas/settings.ts +14 -0
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-JDAF00bC.js +0 -26
- package/dist/ui/assets/index-JDAF00bC.js.map +0 -1
|
@@ -35,6 +35,7 @@ import * as fs from 'node:fs';
|
|
|
35
35
|
import * as path from 'node:path';
|
|
36
36
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
37
37
|
|
|
38
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
38
39
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
39
40
|
|
|
40
41
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -101,48 +102,81 @@ export function trigger_is_meaningful(value: string): boolean {
|
|
|
101
102
|
return true;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
/** Every frontmatter finding for one ADR file. Pure — no I/O, no enumeration. */
|
|
106
|
+
export function check_one(rel: string, text: string): AdrFinding[] {
|
|
107
|
+
const findings: AdrFinding[] = [];
|
|
108
|
+
const fm = parse_frontmatter(text);
|
|
109
|
+
|
|
110
|
+
if (fm === null) {
|
|
111
|
+
findings.push({ file: rel, level: 'error', message: 'no YAML frontmatter block' });
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
114
|
+
for (const req of REQUIRED) {
|
|
115
|
+
if (!fm[req]) findings.push({ file: rel, level: 'error', message: `missing \`${req}\`` });
|
|
116
|
+
}
|
|
117
|
+
if (fm['status'] && !ALLOWED_STATUS.has(fm['status'])) {
|
|
118
|
+
findings.push({
|
|
119
|
+
file: rel,
|
|
120
|
+
level: 'error',
|
|
121
|
+
message: `status \`${fm['status']}\` is not one of: ${[...ALLOWED_STATUS].sort().join(', ')}`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const date = fm['date'] ?? '';
|
|
126
|
+
const grandfathered = date !== '' && date < REVIEW_TRIGGER_SINCE;
|
|
127
|
+
const trigger = fm['review_trigger'];
|
|
128
|
+
|
|
129
|
+
if (!trigger) {
|
|
130
|
+
findings.push({
|
|
131
|
+
file: rel,
|
|
132
|
+
level: grandfathered ? 'warn' : 'error',
|
|
133
|
+
message: grandfathered
|
|
134
|
+
? `no \`review_trigger\` (grandfathered — dated ${date}, before ${REVIEW_TRIGGER_SINCE})`
|
|
135
|
+
: `missing \`review_trigger\` — name the CONDITION that would reopen this decision`,
|
|
136
|
+
});
|
|
137
|
+
} else if (!trigger_is_meaningful(trigger)) {
|
|
138
|
+
findings.push({
|
|
139
|
+
file: rel,
|
|
140
|
+
level: grandfathered ? 'warn' : 'error',
|
|
141
|
+
message: `\`review_trigger\` is a cadence, not a condition: "${trigger}". A calendar review is ignored; an event fires.`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check every ADR in `dir`, optionally recording per-file completeness.
|
|
149
|
+
*
|
|
150
|
+
* The candidate set is every `*.md` in the directory, not the `ADR-*.md` subset:
|
|
151
|
+
* the name filter used to be a bare `continue`, so a decision record saved as
|
|
152
|
+
* `adr-131-foo.md` (lower case) or `131-foo.md` left the corpus without a trace
|
|
153
|
+
* and the gate still printed a green line over the files it did read. It is now
|
|
154
|
+
* an out-of-scope outcome with a reason, and it prints.
|
|
155
|
+
*
|
|
156
|
+
* A `warn`-level finding resolves as `complete`: warnings are allowed by this
|
|
157
|
+
* gate's exit-code contract, so `fail` is reserved for what actually reds it.
|
|
158
|
+
*/
|
|
159
|
+
export function check(dir: string = ADR_DIR, ledger?: GateLedger): AdrFinding[] {
|
|
105
160
|
const findings: AdrFinding[] = [];
|
|
106
161
|
if (!fs.existsSync(dir)) return findings;
|
|
107
162
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const rel = path.relative(REPO_ROOT, path.join(dir, name));
|
|
111
|
-
const fm = parse_frontmatter(fs.readFileSync(path.join(dir, name), 'utf-8'));
|
|
163
|
+
const candidates = fs.readdirSync(dir).filter((n) => n.endsWith('.md')).sort();
|
|
164
|
+
ledger?.plan(candidates);
|
|
112
165
|
|
|
113
|
-
|
|
114
|
-
|
|
166
|
+
for (const name of candidates) {
|
|
167
|
+
if (!/^ADR-.*\.md$/.test(name)) {
|
|
168
|
+
ledger?.outOfScope(name, 'not_applicable_kind');
|
|
115
169
|
continue;
|
|
116
170
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (fm['status'] && !ALLOWED_STATUS.has(fm['status'])) {
|
|
121
|
-
findings.push({
|
|
122
|
-
file: rel,
|
|
123
|
-
level: 'error',
|
|
124
|
-
message: `status \`${fm['status']}\` is not one of: ${[...ALLOWED_STATUS].sort().join(', ')}`,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
171
|
+
const rel = path.relative(REPO_ROOT, path.join(dir, name));
|
|
172
|
+
const own = check_one(rel, fs.readFileSync(path.join(dir, name), 'utf-8'));
|
|
173
|
+
findings.push(...own);
|
|
127
174
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
findings.push({
|
|
134
|
-
file: rel,
|
|
135
|
-
level: grandfathered ? 'warn' : 'error',
|
|
136
|
-
message: grandfathered
|
|
137
|
-
? `no \`review_trigger\` (grandfathered — dated ${date}, before ${REVIEW_TRIGGER_SINCE})`
|
|
138
|
-
: `missing \`review_trigger\` — name the CONDITION that would reopen this decision`,
|
|
139
|
-
});
|
|
140
|
-
} else if (!trigger_is_meaningful(trigger)) {
|
|
141
|
-
findings.push({
|
|
142
|
-
file: rel,
|
|
143
|
-
level: grandfathered ? 'warn' : 'error',
|
|
144
|
-
message: `\`review_trigger\` is a cadence, not a condition: "${trigger}". A calendar review is ignored; an event fires.`,
|
|
145
|
-
});
|
|
175
|
+
const errors = own.filter((f) => f.level === 'error').length;
|
|
176
|
+
if (errors > 0) {
|
|
177
|
+
ledger?.fail(name, `${String(errors)} frontmatter error(s)`);
|
|
178
|
+
} else {
|
|
179
|
+
ledger?.complete(name);
|
|
146
180
|
}
|
|
147
181
|
}
|
|
148
182
|
return findings;
|
|
@@ -150,10 +184,17 @@ export function check(dir: string = ADR_DIR): AdrFinding[] {
|
|
|
150
184
|
|
|
151
185
|
function main(argv: string[]): number {
|
|
152
186
|
const as_json = argv.includes('--json');
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
187
|
+
const ledger = new GateLedger('check_adr_frontmatter');
|
|
188
|
+
const findings = check(ADR_DIR, ledger);
|
|
189
|
+
// Finalized here, printed later by `report()` (which re-finalizes — the call
|
|
190
|
+
// is pure): the denominator is needed for the scope assertion below, which
|
|
191
|
+
// runs before any output. It is the ledger's own per-file accounting — the
|
|
192
|
+
// ADRs that actually reached a checked outcome — where it used to be a SECOND
|
|
193
|
+
// `readdirSync` of the same directory, independent of the loop that did the
|
|
194
|
+
// work, so the printed count could not disagree with the scan even when the
|
|
195
|
+
// scan had skipped files.
|
|
196
|
+
const tally = ledger.finalize();
|
|
197
|
+
const total = tally.completed + tally.failed;
|
|
157
198
|
|
|
158
199
|
// `check()` returns findings, never a count, so a moved `docs/decisions/`
|
|
159
200
|
// yields an empty list — reported as "0 ADR(s) · 0 error(s)" and green.
|
|
@@ -174,7 +215,9 @@ function main(argv: string[]): number {
|
|
|
174
215
|
}
|
|
175
216
|
|
|
176
217
|
if (as_json) {
|
|
177
|
-
|
|
218
|
+
// The tally rides in the payload rather than on stdout: `ledger.report()`
|
|
219
|
+
// would print beside the JSON and break every parser of this mode.
|
|
220
|
+
process.stdout.write(JSON.stringify({ findings, ledger: tally }, null, 2) + '\n');
|
|
178
221
|
return 0;
|
|
179
222
|
}
|
|
180
223
|
|
|
@@ -185,6 +228,7 @@ function main(argv: string[]): number {
|
|
|
185
228
|
`ADR frontmatter: ${total} ADR(s) · ${errors.length} error(s) · ` +
|
|
186
229
|
`${warns.length} grandfathered without a revisit condition\n`,
|
|
187
230
|
);
|
|
231
|
+
ledger.report();
|
|
188
232
|
for (const e of errors) process.stderr.write(` ❌ ${e.file}: ${e.message}\n`);
|
|
189
233
|
|
|
190
234
|
if (errors.length > 0) {
|
|
@@ -41,6 +41,7 @@ import { fileURLToPath } from "node:url";
|
|
|
41
41
|
import { runGateCli, runSelfTest, type SelfTestCase } from "./_lib/gate_self_test.js";
|
|
42
42
|
import { reportScanned } from "./_lib/scan_scope.js";
|
|
43
43
|
|
|
44
|
+
// ledger-exempt: single remote-ref probe — the entire scope is ONE ls-remote answer (0 or 1 refs) resolved to one aggregate ancestor verdict, and every empty path already publishes its reason via reportScanned allowEmpty; there is no per-target collection to account.
|
|
44
45
|
const DEFAULT_BASE = "main";
|
|
45
46
|
const SELF_TEST_MIN_CASES = 3;
|
|
46
47
|
const SELF_TEST_MIN_REJECT = 1;
|
|
@@ -49,6 +49,7 @@ import * as os from 'node:os';
|
|
|
49
49
|
import * as path from 'node:path';
|
|
50
50
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
51
51
|
|
|
52
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
52
53
|
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
53
54
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
54
55
|
|
|
@@ -423,20 +424,32 @@ export interface RunResult {
|
|
|
423
424
|
readonly corpora: number;
|
|
424
425
|
/** CSVs actually opened, reported so "checked nothing" cannot read as clean. */
|
|
425
426
|
readonly csvs: number;
|
|
427
|
+
/** Per-corpus completeness accounting — every discovered corpus reaches one outcome. */
|
|
428
|
+
readonly ledger: GateLedger;
|
|
426
429
|
}
|
|
427
430
|
|
|
428
431
|
export function run_checks(todayStamp: number, skillsDir: string = SKILLS_DIR): RunResult {
|
|
429
432
|
const corpora = discover_corpora(skillsDir);
|
|
433
|
+
const ledger = new GateLedger('check_corpus_staleness');
|
|
434
|
+
ledger.plan(corpora.map((c) => c.slug));
|
|
430
435
|
const findings: CorpusFinding[] = [];
|
|
431
436
|
let csvs = 0;
|
|
432
437
|
for (const corpus of corpora) {
|
|
433
|
-
|
|
434
|
-
|
|
438
|
+
const found: CorpusFinding[] = [
|
|
439
|
+
...check_staleness(corpus, todayStamp),
|
|
440
|
+
...check_attribution_pin(corpus),
|
|
441
|
+
];
|
|
435
442
|
const csv = check_csv_integrity(corpus);
|
|
436
|
-
|
|
443
|
+
found.push(...csv.findings);
|
|
437
444
|
csvs += csv.opened;
|
|
445
|
+
findings.push(...found);
|
|
446
|
+
if (found.length > 0) {
|
|
447
|
+
ledger.fail(corpus.slug, `${String(found.length)} corpus finding(s)`);
|
|
448
|
+
} else {
|
|
449
|
+
ledger.complete(corpus.slug);
|
|
450
|
+
}
|
|
438
451
|
}
|
|
439
|
-
return { findings, corpora: corpora.length, csvs };
|
|
452
|
+
return { findings, corpora: corpora.length, csvs, ledger };
|
|
440
453
|
}
|
|
441
454
|
|
|
442
455
|
const HELP = `check_corpus_staleness — offline staleness + CSV-integrity gate for the grounding corpora
|
|
@@ -651,6 +664,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
651
664
|
|
|
652
665
|
// gate-coverage contract (src/config/gate-coverage.yml): corpora inspected.
|
|
653
666
|
process.stdout.write(`scanned: ${String(result.corpora)}\n`);
|
|
667
|
+
result.ledger.report();
|
|
654
668
|
|
|
655
669
|
if (result.findings.length > 0) {
|
|
656
670
|
process.stdout.write(`❌ ${String(result.findings.length)} corpus finding(s):\n`);
|
|
@@ -28,6 +28,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
28
28
|
|
|
29
29
|
import * as YAML from 'yaml';
|
|
30
30
|
|
|
31
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
31
32
|
import {
|
|
32
33
|
type DefectFinding,
|
|
33
34
|
type TurnSnapshot,
|
|
@@ -163,6 +164,22 @@ export function main(repoRoot: string = process.cwd()): number {
|
|
|
163
164
|
roots: [CORPUS_REL],
|
|
164
165
|
});
|
|
165
166
|
|
|
167
|
+
// Per-detector completeness: every registered detector AND every corpus
|
|
168
|
+
// entry reaches exactly one outcome, so a detector silently absent from the
|
|
169
|
+
// loop above could not read as a pass.
|
|
170
|
+
const ledger = new GateLedger(GATE);
|
|
171
|
+
const names = [...new Set([...Object.keys(DETECTORS), ...Object.keys(corpus)])].sort();
|
|
172
|
+
ledger.plan(names);
|
|
173
|
+
const failing = new Set(findings.map((f) => f.detector));
|
|
174
|
+
for (const name of names) {
|
|
175
|
+
if (failing.has(name)) {
|
|
176
|
+
ledger.fail(name, 'corpus finding(s) against this detector');
|
|
177
|
+
} else {
|
|
178
|
+
ledger.complete(name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
ledger.report();
|
|
182
|
+
|
|
166
183
|
if (findings.length > 0) {
|
|
167
184
|
process.stderr.write(`${GATE}: ${String(findings.length)} finding(s)\n\n`);
|
|
168
185
|
for (const f of findings) {
|
|
@@ -17,6 +17,7 @@ import * as fs from 'node:fs';
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
19
|
|
|
20
|
+
import { GateLedger, LedgerUsageError } from './_lib/gate_ledger.js';
|
|
20
21
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
21
22
|
|
|
22
23
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -108,15 +109,48 @@ function main(argv?: readonly string[]): number {
|
|
|
108
109
|
|
|
109
110
|
const missing: string[] = [];
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
// Per-entry completeness. Three of the four decisions below used to be bare
|
|
113
|
+
// `continue`s, so an entry whose `policy`/`scope` line failed to parse read
|
|
114
|
+
// as "correctly out of scope" and the success line still counted the rest.
|
|
115
|
+
// Each is now a recorded outcome with a reason, and the count printed below
|
|
116
|
+
// is the ledger's, not a second independent filter over the same list.
|
|
117
|
+
const ledger = new GateLedger('check_gitignore_freshness');
|
|
118
|
+
try {
|
|
119
|
+
ledger.plan(entries.map((e) => e.path));
|
|
120
|
+
|
|
121
|
+
for (const e of entries) {
|
|
122
|
+
if (e.policy !== 'ignored') {
|
|
123
|
+
ledger.outOfScope(e.path, 'not_applicable_kind');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (e.scope !== 'consumer' && e.scope !== 'both') {
|
|
127
|
+
ledger.outOfScope(e.path, 'not_applicable_kind');
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Carve-out negations (path starts with !) are separately tracked
|
|
131
|
+
if (e.path.startsWith('!')) {
|
|
132
|
+
ledger.outOfScope(e.path, 'declared_exemption');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!blockLines.has(e.path)) {
|
|
136
|
+
missing.push(e.path);
|
|
137
|
+
ledger.fail(e.path, 'absent from gitignore-block.txt');
|
|
138
|
+
} else {
|
|
139
|
+
ledger.complete(e.path);
|
|
140
|
+
}
|
|
118
141
|
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
// A path listed twice in the manifest makes the denominator wrong in the
|
|
144
|
+
// direction that hides work, so the ledger refuses it. That is a manifest
|
|
145
|
+
// defect, not a missing gitignore line: exit 2 ("the gate could not run"),
|
|
146
|
+
// never 1.
|
|
147
|
+
if (err instanceof LedgerUsageError) {
|
|
148
|
+
process.stderr.write(`❌ ${path.relative(REPO_ROOT, MANIFEST)}: ${err.message}\n`);
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
119
152
|
}
|
|
153
|
+
const tally = ledger.report();
|
|
120
154
|
|
|
121
155
|
if (missing.length > 0) {
|
|
122
156
|
process.stdout.write(
|
|
@@ -134,7 +168,7 @@ function main(argv?: readonly string[]): number {
|
|
|
134
168
|
|
|
135
169
|
if (!quiet) {
|
|
136
170
|
process.stdout.write(
|
|
137
|
-
`✅ gitignore-block.txt covers all ${
|
|
171
|
+
`✅ gitignore-block.txt covers all ${tally.completed + tally.failed} consumer-scoped ignored paths.\n`,
|
|
138
172
|
);
|
|
139
173
|
}
|
|
140
174
|
return 0;
|
|
@@ -44,6 +44,8 @@ import { fileURLToPath } from "node:url";
|
|
|
44
44
|
|
|
45
45
|
import { parse as parseYaml } from "yaml";
|
|
46
46
|
|
|
47
|
+
// Single-quoted on purpose: check_gate_completeness detects ledger adoption by this exact import form.
|
|
48
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
47
49
|
import { reportScanned } from "./_lib/scan_scope.js";
|
|
48
50
|
|
|
49
51
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -135,6 +137,24 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
135
137
|
: {}),
|
|
136
138
|
});
|
|
137
139
|
|
|
140
|
+
// Per-source completeness: every bundled source reaches exactly one outcome
|
|
141
|
+
// (fresh or stale), so a source silently dropped from the mtime sweep could
|
|
142
|
+
// not read as a pass. Nothing to plan when no self-hosted bundle exists.
|
|
143
|
+
if (!r.skipped) {
|
|
144
|
+
const ledger = new GateLedger("check_hook_bundle_freshness");
|
|
145
|
+
const sources = bundledSources(root);
|
|
146
|
+
ledger.plan(sources);
|
|
147
|
+
const staleSet = new Set(r.stale.map((s) => s.file));
|
|
148
|
+
for (const file of sources) {
|
|
149
|
+
if (staleSet.has(file)) {
|
|
150
|
+
ledger.fail(file, "source is newer than the built bundle");
|
|
151
|
+
} else {
|
|
152
|
+
ledger.complete(file);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
ledger.report();
|
|
156
|
+
}
|
|
157
|
+
|
|
138
158
|
// A gate that scans nothing must SAY it scanned nothing — a silent green
|
|
139
159
|
// here is indistinguishable from a pass.
|
|
140
160
|
if (r.skipped) {
|
|
@@ -39,6 +39,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
39
39
|
|
|
40
40
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
41
|
|
|
42
|
+
// ledger-exempt: single folded-hash comparison — the ordered kernel bodies become ONE sha256 via compute_prefix, byte-compared against the single committed snapshot, so the gate has exactly one aggregate drift verdict and no per-rule outcome exists to record; a changed byte in any body is indistinguishable from a reorder by construction, which IS the invariant. The kernel-count scope floor is already asserted by assertScanned over dist/router.json.
|
|
42
43
|
const _HERE = fileURLToPath(import.meta.url);
|
|
43
44
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
44
45
|
const ROUTER = path.join(REPO_ROOT, 'dist/router.json');
|
|
@@ -114,7 +114,7 @@ export function reasonIsStated(reason: string): boolean {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/** One measured layer. */
|
|
117
|
-
interface LayerMeasure {
|
|
117
|
+
export interface LayerMeasure {
|
|
118
118
|
readonly label: string;
|
|
119
119
|
readonly dir: string;
|
|
120
120
|
readonly files: number;
|
|
@@ -122,17 +122,66 @@ interface LayerMeasure {
|
|
|
122
122
|
readonly exact: boolean;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
function measureFiles(label: string, dir: string, files: ReadonlyMap<string, string>): LayerMeasure {
|
|
126
|
+
const m = measure([...files.values()].join('\n'));
|
|
127
|
+
return { label, dir, files: files.size, tokens: m.tokens_gpt, exact: m.tokens_gpt_exact };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The one measurement both the gate (`main` below) and the doctor surface
|
|
132
|
+
* (`routing_doctor`) consume — per-layer rule tokens, the overlap between the
|
|
133
|
+
* two layers, and which input the numbers came from. Exported so the doctor
|
|
134
|
+
* reuses it instead of growing a second, drifting copy.
|
|
135
|
+
*/
|
|
136
|
+
export interface StandingDeliveryMeasure {
|
|
137
|
+
/** Which input produced the numbers — see `instructionsLoadedRecord`. */
|
|
138
|
+
readonly input: 'instructions-loaded' | 'filesystem';
|
|
139
|
+
readonly layers: readonly LayerMeasure[];
|
|
140
|
+
/** Sum over present layers — what the session receives under the host's no-dedup contract. */
|
|
141
|
+
readonly received_tokens: number;
|
|
142
|
+
/** Rules present in BOTH layers (basename match). */
|
|
143
|
+
readonly overlap_rules: number;
|
|
144
|
+
readonly duplicate_rules: number;
|
|
145
|
+
readonly divergent_rules: number;
|
|
146
|
+
/** Tokens of the project-layer copies of overlapping rules — the doubled delivery. */
|
|
147
|
+
readonly overlap_tokens: number;
|
|
148
|
+
readonly exact: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Measure both layers once. `null` when neither directory exists (nothing installed). */
|
|
152
|
+
export function measureStandingDelivery(
|
|
153
|
+
global_dir: string,
|
|
154
|
+
project_dir: string,
|
|
155
|
+
record_path: string,
|
|
156
|
+
): StandingDeliveryMeasure | null {
|
|
157
|
+
const g = rule_layer_overlap.readRuleLayer(global_dir);
|
|
158
|
+
const p = rule_layer_overlap.readRuleLayer(project_dir);
|
|
159
|
+
if (g === null && p === null) return null;
|
|
160
|
+
const layers: LayerMeasure[] = [];
|
|
161
|
+
if (g !== null) layers.push(measureFiles('global', global_dir, g.files));
|
|
162
|
+
if (p !== null) layers.push(measureFiles('project', project_dir, p.files));
|
|
163
|
+
let overlap_rules = 0;
|
|
164
|
+
let duplicate_rules = 0;
|
|
165
|
+
let divergent_rules = 0;
|
|
166
|
+
let overlap_tokens = 0;
|
|
167
|
+
if (g !== null && p !== null) {
|
|
168
|
+
const r = rule_layer_overlap.compareLayers(g.files, p.files);
|
|
169
|
+
overlap_rules = r.overlap.length;
|
|
170
|
+
duplicate_rules = r.duplicate.length;
|
|
171
|
+
divergent_rules = r.divergent.length;
|
|
172
|
+
if (r.overlap.length > 0) {
|
|
173
|
+
overlap_tokens = measure(r.overlap.map((n) => p.files.get(n) ?? '').join('\n')).tokens_gpt;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
130
176
|
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
input: fs.existsSync(record_path) ? 'instructions-loaded' : 'filesystem',
|
|
178
|
+
layers,
|
|
179
|
+
received_tokens: layers.reduce((n, l) => n + l.tokens, 0),
|
|
180
|
+
overlap_rules,
|
|
181
|
+
duplicate_rules,
|
|
182
|
+
divergent_rules,
|
|
183
|
+
overlap_tokens,
|
|
184
|
+
exact: layers.every((l) => l.exact),
|
|
136
185
|
};
|
|
137
186
|
}
|
|
138
187
|
|
|
@@ -185,10 +234,12 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
185
234
|
return 1;
|
|
186
235
|
}
|
|
187
236
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
237
|
+
const measured = measureStandingDelivery(
|
|
238
|
+
path.join(os.homedir(), '.claude', 'rules'),
|
|
239
|
+
path.join(REPO_ROOT, '.claude', 'rules'),
|
|
240
|
+
instructionsLoadedRecord(REPO_ROOT),
|
|
241
|
+
);
|
|
242
|
+
const layers = measured?.layers ?? [];
|
|
192
243
|
|
|
193
244
|
const scanned = layers.reduce((n, l) => n + l.files, 0);
|
|
194
245
|
// Two zeroes that mean opposite things, and conflating them is how this gate
|
|
@@ -202,7 +253,7 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
202
253
|
// - **A layer directory that exists and holds no `.md`** — a projection that
|
|
203
254
|
// ran and produced nothing. That is blindness, not cleanliness, and it
|
|
204
255
|
// stays exit 3.
|
|
205
|
-
const no_layer_present =
|
|
256
|
+
const no_layer_present = measured === null;
|
|
206
257
|
try {
|
|
207
258
|
assertScanned({
|
|
208
259
|
gate: PROG,
|
|
@@ -239,10 +290,7 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
239
290
|
return 0;
|
|
240
291
|
}
|
|
241
292
|
|
|
242
|
-
const
|
|
243
|
-
const input = fs.existsSync(record) ? 'instructions-loaded' : 'filesystem';
|
|
244
|
-
const total = layers.reduce((n, l) => n + l.tokens, 0);
|
|
245
|
-
const exact = layers.every((l) => l.exact);
|
|
293
|
+
const { input, received_tokens: total, exact } = measured;
|
|
246
294
|
|
|
247
295
|
out(`${PROG} · input: ${input} · ${method_note()}\n`);
|
|
248
296
|
if (input === 'filesystem') {
|
|
@@ -256,18 +304,11 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
256
304
|
out(` ${l.label.padEnd(8)} ${String(l.files).padStart(4)} file(s) ${String(l.tokens).padStart(8)} tok\n`);
|
|
257
305
|
}
|
|
258
306
|
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (r.overlap.length > 0) {
|
|
265
|
-
out(
|
|
266
|
-
` overlap ${String(r.overlap.length).padStart(4)} rule(s) in both layers `
|
|
267
|
-
+ `(${r.duplicate.length} duplicate, ${r.divergent.length} divergent)\n`,
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
307
|
+
if (measured.overlap_rules > 0) {
|
|
308
|
+
out(
|
|
309
|
+
` overlap ${String(measured.overlap_rules).padStart(4)} rule(s) in both layers `
|
|
310
|
+
+ `(${measured.duplicate_rules} duplicate, ${measured.divergent_rules} divergent)\n`,
|
|
311
|
+
);
|
|
271
312
|
}
|
|
272
313
|
|
|
273
314
|
const pct = total / budget.total_cap_tokens;
|