@event4u/agent-config 9.29.0 → 9.30.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 +108 -7
- 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-OJM3MBJM.js → chunk-BTJNRUX3.js} +21 -34
- package/dist/cli-delegate/{chunk-MMXA5GY7.js → chunk-DDUNNFYX.js} +14 -0
- package/dist/cli-delegate/{chunk-NZ5N2XQL.js → chunk-EQY4FLJB.js} +1 -1
- package/dist/cli-delegate/{chunk-RCIA67XN.js → chunk-IYTGIG2Y.js} +44 -1
- package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- package/dist/cli-delegate/cmd_conformance.js +2798 -95
- package/dist/cli-delegate/cmd_doctor.js +2 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +86 -6
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +2 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- 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 +1904 -575
- package/dist/install/install.mjs +14 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3760 -726
- 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/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/_dispatch.bash +23 -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 +507 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +89 -0
- 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 +194 -0
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/rebuild_hook_bundle.ts +1 -0
- package/src/scripts/report_conformance_funnel.ts +284 -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
|
@@ -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;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Static-layer stability lint (road-to-token-economy-cache Phase 2.2 — the
|
|
4
|
+
* hygiene half the Phase-1 null left standing; spike note:
|
|
5
|
+
* agents/settings/contexts/cache-injection-anatomy.md).
|
|
6
|
+
*
|
|
7
|
+
* The always-loaded layer is re-sent on every session and every spawn; a
|
|
8
|
+
* MACHINE-VOLATILE marker inside it (an absolute local path naming a real
|
|
9
|
+
* user, a session/run-id shape) is both a cache-stability hazard and a
|
|
10
|
+
* portability leak. Scope is deliberately NARROW and honest:
|
|
11
|
+
*
|
|
12
|
+
* - scanned: dist/agent-src/rules/*.md EXCLUDING the kernel set (the
|
|
13
|
+
* kernel prefix has its own byte-drift guard,
|
|
14
|
+
* check_kernel_prefix_stability — no double coverage, roadmap Risk 8)
|
|
15
|
+
* - flagged: absolute home paths (/Users/<name>/, /home/<name>/,
|
|
16
|
+
* C:\Users\<name>\) and UUID-shaped run/session ids
|
|
17
|
+
* - NOT flagged: dates in prose (stable content, not build-volatile) —
|
|
18
|
+
* build-time volatility is already impossible for .md by the ADR-201
|
|
19
|
+
* byte-exact projection (dist == rewrite(src)); this lint catches what
|
|
20
|
+
* an AUTHOR pastes, which the projection faithfully preserves.
|
|
21
|
+
*
|
|
22
|
+
* Exit 0 green · 1 findings · 2 unusable. Dead-scope guarded via
|
|
23
|
+
* assertScanned. Self-test: tests/scripts/check_static_layer_stability.test.ts
|
|
24
|
+
* proves red on a fixture carrying a home path and a UUID.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
|
|
30
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
31
|
+
|
|
32
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
33
|
+
const _IN_BUNDLE = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
34
|
+
const REPO_ROOT = path.resolve(
|
|
35
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
36
|
+
...(_IN_BUNDLE ? ['..'] : ['..', '..']),
|
|
37
|
+
);
|
|
38
|
+
const RULES_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'rules');
|
|
39
|
+
const ROUTER = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
40
|
+
|
|
41
|
+
/** Machine-volatile shapes. Home paths anchor on the OS home prefix; the
|
|
42
|
+
* UUID shape requires all five groups so ordinary hex constants stay quiet. */
|
|
43
|
+
const HOME_PATH_RE = /(?:\/(?:Users|home)\/[A-Za-z][\w.-]*\/|[A-Za-z]:\\Users\\[A-Za-z][\w.-]*\\)/;
|
|
44
|
+
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
45
|
+
/** Per-line opt-out for a deliberate, justified exception. */
|
|
46
|
+
const IGNORE_MARKER = 'static-layer-allow';
|
|
47
|
+
|
|
48
|
+
export interface Finding {
|
|
49
|
+
file: string;
|
|
50
|
+
line: number;
|
|
51
|
+
kind: 'home-path' | 'uuid';
|
|
52
|
+
excerpt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function kernelBasenames(routerPath: string): Set<string> {
|
|
56
|
+
try {
|
|
57
|
+
const router = JSON.parse(fs.readFileSync(routerPath, 'utf8')) as { kernel?: unknown };
|
|
58
|
+
if (Array.isArray(router.kernel)) {
|
|
59
|
+
return new Set(
|
|
60
|
+
router.kernel
|
|
61
|
+
.filter((k): k is string => typeof k === 'string')
|
|
62
|
+
.map((k) => (k.endsWith('.md') ? k : `${k}.md`)),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
/* fall through — an unreadable router means we scan everything (fail toward coverage) */
|
|
67
|
+
}
|
|
68
|
+
return new Set();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function scan(opts: { rulesDir?: string; routerPath?: string } = {}): { findings: Finding[]; scanned: number } {
|
|
72
|
+
const rulesDir = opts.rulesDir ?? RULES_DIR;
|
|
73
|
+
const kernel = kernelBasenames(opts.routerPath ?? ROUTER);
|
|
74
|
+
const findings: Finding[] = [];
|
|
75
|
+
let scanned = 0;
|
|
76
|
+
const files = fs.existsSync(rulesDir) ? fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md')) : [];
|
|
77
|
+
for (const f of files) {
|
|
78
|
+
if (kernel.has(f)) continue; // kernel prefix has its own drift guard
|
|
79
|
+
scanned += 1;
|
|
80
|
+
const text = fs.readFileSync(path.join(rulesDir, f), 'utf8');
|
|
81
|
+
const lines = text.split('\n');
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i] ?? '';
|
|
84
|
+
if (line.includes(IGNORE_MARKER)) continue;
|
|
85
|
+
if (HOME_PATH_RE.test(line)) {
|
|
86
|
+
findings.push({ file: f, line: i + 1, kind: 'home-path', excerpt: line.trim().slice(0, 120) });
|
|
87
|
+
} else if (UUID_RE.test(line)) {
|
|
88
|
+
findings.push({ file: f, line: i + 1, kind: 'uuid', excerpt: line.trim().slice(0, 120) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { findings, scanned };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function main(argv: readonly string[]): number {
|
|
96
|
+
let rulesDir: string | undefined;
|
|
97
|
+
let routerPath: string | undefined;
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
if (argv[i] === '--rules-dir') rulesDir = argv[++i];
|
|
100
|
+
else if (argv[i] === '--router') routerPath = argv[++i];
|
|
101
|
+
}
|
|
102
|
+
const opts: Parameters<typeof scan>[0] = {
|
|
103
|
+
...(rulesDir !== undefined ? { rulesDir } : {}),
|
|
104
|
+
...(routerPath !== undefined ? { routerPath } : {}),
|
|
105
|
+
};
|
|
106
|
+
let result: ReturnType<typeof scan>;
|
|
107
|
+
try {
|
|
108
|
+
result = scan(opts);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
process.stderr.write(`check_static_layer_stability: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
assertScanned({
|
|
115
|
+
gate: 'check_static_layer_stability',
|
|
116
|
+
scanned: result.scanned,
|
|
117
|
+
units: 'non-kernel always-loaded rule file(s)',
|
|
118
|
+
roots: [rulesDir ?? RULES_DIR],
|
|
119
|
+
});
|
|
120
|
+
} catch (exc) {
|
|
121
|
+
if (exc instanceof DeadScopeError) {
|
|
122
|
+
process.stderr.write(`error: ${exc.message}\n`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
throw exc;
|
|
126
|
+
}
|
|
127
|
+
for (const f of result.findings) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`❌ ${f.file}:${f.line} — ${f.kind} in the always-loaded layer (machine-volatile marker; re-sent on every spawn): ${f.excerpt}\n`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (result.findings.length > 0) {
|
|
133
|
+
process.stderr.write(
|
|
134
|
+
`check_static_layer_stability: ${result.findings.length} finding(s) across ${result.scanned} scanned file(s). ` +
|
|
135
|
+
`A deliberate exception carries '${IGNORE_MARKER}' on the line with its reason.\n`,
|
|
136
|
+
);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
process.stdout.write(`✅ check_static_layer_stability: ${result.scanned} non-kernel always-loaded file(s) clean\n`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _isCliEntry(): boolean {
|
|
144
|
+
if (_IN_BUNDLE) return false;
|
|
145
|
+
if (process.argv[1] === undefined) return false;
|
|
146
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
147
|
+
if (import.meta.url === argvUrl) return true;
|
|
148
|
+
try {
|
|
149
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (_isCliEntry()) process.exit(main(process.argv.slice(2)));
|
|
@@ -117,6 +117,24 @@ export interface SuppressionSpec {
|
|
|
117
117
|
* migration to content anchors visible instead of aspirational.
|
|
118
118
|
*/
|
|
119
119
|
driftKey?: { positionField: string; anchorField: string };
|
|
120
|
+
/**
|
|
121
|
+
* Accept this baseline as absent at the base ref — for the ONE change that
|
|
122
|
+
* introduces it, then removed.
|
|
123
|
+
*
|
|
124
|
+
* Without it a brand-new baseline is a permanent red: the ratchet compares
|
|
125
|
+
* against `origin/main`, where the file does not exist yet, and
|
|
126
|
+
* `ratchet_base_ref` is deliberately fail-closed there because
|
|
127
|
+
* absent-at-base and mistyped-path look identical from inside. Setting it
|
|
128
|
+
* makes the verdict `new_baseline` rather than growth, so the bootstrap
|
|
129
|
+
* entries are not each reported as a new hole.
|
|
130
|
+
*
|
|
131
|
+
* Remove the flag once the introducing commit is an ancestor of the base
|
|
132
|
+
* ref — leaving it set would silently accept a later mistyped path. That
|
|
133
|
+
* removal is **enforced, not remembered**: the scan below fails when the
|
|
134
|
+
* flag is set and the baseline already resolves at the base ref, so the
|
|
135
|
+
* flag closes itself one merge after it was needed.
|
|
136
|
+
*/
|
|
137
|
+
newInThisChange?: boolean;
|
|
120
138
|
/** Why this list exists at all — printed with the count, so absence is visible. */
|
|
121
139
|
what: string;
|
|
122
140
|
}
|
|
@@ -199,6 +217,19 @@ export const SUPPRESSION_INVENTORY: readonly SuppressionSpec[] = [
|
|
|
199
217
|
growth: 'requires_falsifier',
|
|
200
218
|
what: 'per-gate known-debt counts',
|
|
201
219
|
},
|
|
220
|
+
{
|
|
221
|
+
// The root IS the list — a bare array of rule filenames, so entries are
|
|
222
|
+
// content-keyed by construction and cannot drift on line numbers.
|
|
223
|
+
// `forbidden` growth is the whole point: this baseline exists so a NEW
|
|
224
|
+
// rule cannot join the silent set, and `lint_rule_enforcement_declaration`
|
|
225
|
+
// additionally fails when an entry stops being undeclared, so the list is
|
|
226
|
+
// shrink-only from both directions.
|
|
227
|
+
file: 'src/config/rule-enforcement-baseline.json',
|
|
228
|
+
listKey: null,
|
|
229
|
+
tier: 'string_list',
|
|
230
|
+
newInThisChange: true,
|
|
231
|
+
what: 'rules predating the enforced_by-declaration ratchet',
|
|
232
|
+
},
|
|
202
233
|
];
|
|
203
234
|
|
|
204
235
|
export interface SuppressionFinding {
|
|
@@ -367,7 +398,24 @@ export function main(): number {
|
|
|
367
398
|
baseRef,
|
|
368
399
|
repoRoot: REPO_ROOT,
|
|
369
400
|
entriesOf: (parsed) => entriesOfSpec(spec, parsed),
|
|
401
|
+
...(spec.newInThisChange === true ? { allowNewBaseline: true } : {}),
|
|
370
402
|
});
|
|
403
|
+
// Self-closing bootstrap flag. `newInThisChange` exists for the ONE
|
|
404
|
+
// change that introduces a baseline, and the comment on the field says
|
|
405
|
+
// to remove it afterwards — which is exactly the kind of instruction
|
|
406
|
+
// that gets forgotten, because nothing goes red when it is ignored and
|
|
407
|
+
// a stale flag silently accepts a later mistyped path. So the check
|
|
408
|
+
// closes itself: once the baseline resolves at the base ref, the
|
|
409
|
+
// verdict is no longer `new_baseline`, and keeping the flag is a
|
|
410
|
+
// finding rather than a note in someone's follow-up list.
|
|
411
|
+
if (spec.newInThisChange === true && comparison.verdict !== 'new_baseline') {
|
|
412
|
+
const detail =
|
|
413
|
+
`\`newInThisChange\` is still set, but the baseline now resolves at ${baseRef} — ` +
|
|
414
|
+
'the flag has served its purpose and must be removed from SUPPRESSION_INVENTORY. ' +
|
|
415
|
+
'Leaving it set makes a future mistyped path pass as a new baseline forever.';
|
|
416
|
+
ledger.fail(spec.file, detail);
|
|
417
|
+
findings.push({ file: spec.file, entry: '(whole file)', kind: 'uninventoried', detail });
|
|
418
|
+
}
|
|
371
419
|
} catch (exc) {
|
|
372
420
|
if (exc instanceof BaseRefUnavailableError) {
|
|
373
421
|
ledger.fail(spec.file, exc.message);
|