@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
|
@@ -145,7 +145,7 @@ fi
|
|
|
145
145
|
# Roadmap dashboard sync — only fires when staged changes touch a roadmap
|
|
146
146
|
# file or the dashboard itself, so unrelated commits stay fast.
|
|
147
147
|
if git diff --cached --name-only | grep -qE '^agents/roadmaps(-progress\.md|/)'; then
|
|
148
|
-
./scripts-run
|
|
148
|
+
./scripts-run src/scripts/roadmap_progress_hook --check
|
|
149
149
|
rstatus=$?
|
|
150
150
|
if [ $rstatus -ne 0 ]; then
|
|
151
151
|
# The check fails for THREE distinct reasons and prints the real one
|
|
@@ -72,7 +72,7 @@ import fs from "node:fs";
|
|
|
72
72
|
import path from "node:path";
|
|
73
73
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
74
74
|
|
|
75
|
-
import { atomic_write_json } from "./hooks/state_io.js";
|
|
75
|
+
import { atomic_write_json, atomic_write_text, is_replay_mode } from "./hooks/state_io.js";
|
|
76
76
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
77
77
|
import { humanAuthoredLead, isSyntheticPrompt } from "./_lib/prompt_shape.js";
|
|
78
78
|
|
|
@@ -87,6 +87,27 @@ const EXIT_WARN = 2;
|
|
|
87
87
|
|
|
88
88
|
export const STATE_FILE = path.join("agents", "state", "language-mirror.json");
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Marker written at `pre_compact` and cleared by the single `post_tool_use`
|
|
92
|
+
* re-emit that follows it (round-5 § 6.1). Its existence — not a counter — is
|
|
93
|
+
* what bounds the re-injection to ONE per compaction event.
|
|
94
|
+
*/
|
|
95
|
+
export const PIN_LOST_DIR = path.join("agents", "state", "language-mirror.pin-lost");
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Per-SESSION marker path. R2 finding 10: a single shared marker made the
|
|
99
|
+
* mechanism cross-talk between concurrent sessions — the normal shape for this
|
|
100
|
+
* repo's worktree workflow. Session B's next `post_tool_use` consumed the marker
|
|
101
|
+
* session A's compaction had set, so A never got the one re-emit that is the
|
|
102
|
+
* whole point of the mechanism, and B got a pin notice derived from whichever
|
|
103
|
+
* session last wrote the shared state. An empty id degrades to a literal
|
|
104
|
+
* bucket rather than colliding silently.
|
|
105
|
+
*/
|
|
106
|
+
export function pinLostMarker(session_id: string): string {
|
|
107
|
+
const safe = (session_id || "unknown-session").replace(/[^A-Za-z0-9._-]/g, "_");
|
|
108
|
+
return path.join(PIN_LOST_DIR, `${safe}.marker`);
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
91
112
|
type JsonObject = { [k: string]: JsonValue };
|
|
92
113
|
|
|
@@ -380,6 +401,103 @@ function _extractPrompt(payload: JsonObject): string {
|
|
|
380
401
|
return "";
|
|
381
402
|
}
|
|
382
403
|
|
|
404
|
+
/** The envelope's session id, or a literal bucket when the host sends none. */
|
|
405
|
+
function _sessionId(envelope: JsonObject): string {
|
|
406
|
+
const v = envelope["session_id"];
|
|
407
|
+
return typeof v === "string" && v.trim() !== "" ? v : "unknown-session";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Record that compaction is about to drop the context copy of the pin.
|
|
412
|
+
*
|
|
413
|
+
* R2 finding 14: this used to write with a raw `mkdirSync` + `writeFileSync`,
|
|
414
|
+
* bypassing both conventions the surrounding code follows — the module's own
|
|
415
|
+
* atomic writer, and the `is_replay_mode()` guard that keeps a replayed event
|
|
416
|
+
* from mutating live state. A replayed compaction wrote the marker for real.
|
|
417
|
+
*/
|
|
418
|
+
/**
|
|
419
|
+
* The pre-R2 layout wrote a single FILE at the path that is now a DIRECTORY.
|
|
420
|
+
* A consumer upgrading with a leftover legacy marker — compaction happened, no
|
|
421
|
+
* `post_tool_use` followed — hits `EEXIST` on the mkdir and `ENOTDIR` on the
|
|
422
|
+
* write (reproduced against Node), which the catch below swallows as
|
|
423
|
+
* "observability only". The failure would then be silent AND permanent for that
|
|
424
|
+
* root: `existsSync` always false, so the mechanism is dead with no diagnostic.
|
|
425
|
+
* One unlink at the moment of collision, and only when the path is a file.
|
|
426
|
+
*/
|
|
427
|
+
function _migrateLegacyMarker(consumer_root: string): void {
|
|
428
|
+
const legacy = path.join(consumer_root, PIN_LOST_DIR);
|
|
429
|
+
try {
|
|
430
|
+
if (fs.statSync(legacy).isFile()) {
|
|
431
|
+
fs.rmSync(legacy, { force: true });
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
434
|
+
// Absent, already a directory, or unreadable — nothing to migrate.
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function _setPinLost(consumer_root: string, session_id: string): void {
|
|
439
|
+
if (is_replay_mode()) return;
|
|
440
|
+
_migrateLegacyMarker(consumer_root);
|
|
441
|
+
try {
|
|
442
|
+
atomic_write_text(path.join(consumer_root, pinLostMarker(session_id)), new Date().toISOString());
|
|
443
|
+
} catch {
|
|
444
|
+
// Observability only — a failed marker write costs one un-restored pin,
|
|
445
|
+
// never a broken turn.
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function _pinLost(consumer_root: string, session_id: string): boolean {
|
|
450
|
+
try {
|
|
451
|
+
return fs.existsSync(path.join(consumer_root, pinLostMarker(session_id)));
|
|
452
|
+
} catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function _clearPinLost(consumer_root: string, session_id: string): void {
|
|
458
|
+
// R2 round 2, finding 8: the replay guard covered only the WRITE. A replayed
|
|
459
|
+
// `post_tool_use` therefore deleted a live marker it was forbidden to create,
|
|
460
|
+
// and the real session lost the single re-emit this whole mechanism exists to
|
|
461
|
+
// deliver — replay destroying live state while blocked from writing it, which
|
|
462
|
+
// is worse than either direction alone.
|
|
463
|
+
if (is_replay_mode()) return;
|
|
464
|
+
try {
|
|
465
|
+
fs.rmSync(path.join(consumer_root, pinLostMarker(session_id)), { force: true });
|
|
466
|
+
} catch {
|
|
467
|
+
// If it cannot be cleared the next tool call re-emits once more. One
|
|
468
|
+
// duplicate line is the failure mode; a loop is not, because the marker
|
|
469
|
+
// is only ever SET by a compaction event.
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The `post_tool_use` half. Emits the pin exactly once per compaction event
|
|
475
|
+
* and only when a pin actually exists — an absent or undetermined pin is not
|
|
476
|
+
* an obligation, and inventing one here would be worse than the gap.
|
|
477
|
+
*/
|
|
478
|
+
export function _reEmitAfterCompaction(consumer_root: string, session_id: string): number {
|
|
479
|
+
if (!_pinLost(consumer_root, session_id)) {
|
|
480
|
+
return EXIT_ALLOW;
|
|
481
|
+
}
|
|
482
|
+
const previous = _loadState(path.join(consumer_root, STATE_FILE));
|
|
483
|
+
const language = previous.language as Verdict | undefined;
|
|
484
|
+
const source: PinSource = previous.source === "system-locale" ? "system-locale" : "prompt";
|
|
485
|
+
// Clear FIRST: if the write below throws, the marker must not survive to
|
|
486
|
+
// re-fire on every subsequent tool call — that is the shape § 6.2 refuses.
|
|
487
|
+
_clearPinLost(consumer_root, session_id);
|
|
488
|
+
if (language !== "de" && language !== "en") {
|
|
489
|
+
return EXIT_ALLOW;
|
|
490
|
+
}
|
|
491
|
+
process.stdout.write(
|
|
492
|
+
`${JSON.stringify({
|
|
493
|
+
decision: "warn",
|
|
494
|
+
reason: noticeText(language, source),
|
|
495
|
+
additional_context: pinText(language, source),
|
|
496
|
+
})}\n`,
|
|
497
|
+
);
|
|
498
|
+
return EXIT_WARN;
|
|
499
|
+
}
|
|
500
|
+
|
|
383
501
|
export function run(
|
|
384
502
|
stdin_text: string,
|
|
385
503
|
options: { consumer_root: string; env?: NodeJS.ProcessEnv },
|
|
@@ -397,6 +515,30 @@ export function run(
|
|
|
397
515
|
}
|
|
398
516
|
|
|
399
517
|
const payload = _isObject(envelope["payload"]) ? (envelope["payload"] as JsonObject) : envelope;
|
|
518
|
+
|
|
519
|
+
// round-5 § 6.1 — the compaction boundary.
|
|
520
|
+
//
|
|
521
|
+
// 4 of the 23 post-merge violations happened with NO pin in context: a
|
|
522
|
+
// `compact_boundary` fired and the first violation followed 26 seconds
|
|
523
|
+
// later. Compaction removes the context copy of the pin, not this hook's
|
|
524
|
+
// state file — so this is a state defect with a deterministic fix, aimed
|
|
525
|
+
// only at the subset where the pin was measurably MISSING, never at the 19
|
|
526
|
+
// where it was measurably ignored.
|
|
527
|
+
//
|
|
528
|
+
// The guard IS the design, and is not optional: `pre_compact` sets a
|
|
529
|
+
// marker, `post_tool_use` re-emits once while it is set and clears it. That
|
|
530
|
+
// is one extra injection per compaction EVENT. A re-pin on every tool call
|
|
531
|
+
// is what § 6.2 refuses — "the same failed mechanism running more often" —
|
|
532
|
+
// and this marker is the entire difference between the two.
|
|
533
|
+
const event = typeof envelope["event"] === "string" ? envelope["event"] : "";
|
|
534
|
+
if (event === "pre_compact") {
|
|
535
|
+
_setPinLost(options.consumer_root, _sessionId(envelope));
|
|
536
|
+
return EXIT_ALLOW;
|
|
537
|
+
}
|
|
538
|
+
if (event === "post_tool_use") {
|
|
539
|
+
return _reEmitAfterCompaction(options.consumer_root, _sessionId(envelope));
|
|
540
|
+
}
|
|
541
|
+
|
|
400
542
|
const prompt = _extractPrompt(payload);
|
|
401
543
|
if (!prompt) {
|
|
402
544
|
return EXIT_ALLOW;
|
|
@@ -41,6 +41,7 @@ import * as path from 'node:path';
|
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
42
|
import { parse as parseYaml } from 'yaml';
|
|
43
43
|
|
|
44
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
44
45
|
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
45
46
|
import { reportScanned } from './_lib/scan_scope.js';
|
|
46
47
|
|
|
@@ -188,15 +189,26 @@ export function main(argv?: readonly string[]): number {
|
|
|
188
189
|
names = [];
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
// Per-rule completeness: every listed rule reaches exactly one outcome, so
|
|
193
|
+
// the frontmatter-less `continue` below is a named skip, not a silent one.
|
|
194
|
+
const ledger = new GateLedger('lint_decision_impact');
|
|
195
|
+
ledger.plan(names);
|
|
191
196
|
const violations: Violation[] = [];
|
|
192
197
|
let scanned = 0;
|
|
193
198
|
for (const n of names) {
|
|
194
199
|
const fm = frontmatterOf(fs.readFileSync(path.join(dir, n), 'utf-8'));
|
|
195
200
|
if (fm === null) {
|
|
201
|
+
ledger.skip(n, 'not_applicable_kind');
|
|
196
202
|
continue;
|
|
197
203
|
}
|
|
198
204
|
scanned += 1;
|
|
199
|
-
|
|
205
|
+
const found = checkRule(n.slice(0, -3), fm);
|
|
206
|
+
violations.push(...found);
|
|
207
|
+
if (found.length > 0) {
|
|
208
|
+
ledger.fail(n, `${String(found.length)} issue(s)`);
|
|
209
|
+
} else {
|
|
210
|
+
ledger.complete(n);
|
|
211
|
+
}
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
if (violations.length > 0) {
|
|
@@ -214,6 +226,7 @@ export function main(argv?: readonly string[]): number {
|
|
|
214
226
|
);
|
|
215
227
|
}
|
|
216
228
|
|
|
229
|
+
ledger.report();
|
|
217
230
|
reportScanned({
|
|
218
231
|
gate: 'lint_decision_impact',
|
|
219
232
|
scanned,
|
|
@@ -318,6 +318,87 @@ function _check_dead_concerns(
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Role-axis validation (road-to-token-economy-dispatch Phase 2). Errors on:
|
|
323
|
+
* - a `roles` block that is not a mapping of role → { drop: [names] }
|
|
324
|
+
* - a drop entry naming an unknown concern
|
|
325
|
+
* - a drop entry naming a concern bound to ANY platform's `pre_tool_use`
|
|
326
|
+
* slot — the safety-guard slot is undroppable (Phase 2.3: "the manifest
|
|
327
|
+
* diff must show zero pre_tool_use guard removals, CI-checked"). The
|
|
328
|
+
* dispatcher also refuses pre_tool_use drops at runtime; this check
|
|
329
|
+
* makes the attempt a red build instead of a silent no-op.
|
|
330
|
+
*/
|
|
331
|
+
export function _check_roles(
|
|
332
|
+
manifest: YamlObject,
|
|
333
|
+
concernNames: Set<string>,
|
|
334
|
+
errors: string[],
|
|
335
|
+
): void {
|
|
336
|
+
const rolesRaw = manifest["roles"];
|
|
337
|
+
if (rolesRaw === undefined || rolesRaw === null) {
|
|
338
|
+
return; // no role axis — valid (every chain is the orchestrator default)
|
|
339
|
+
}
|
|
340
|
+
if (!isObject(rolesRaw)) {
|
|
341
|
+
errors.push(`roles: must be a mapping, got ${pyTypeName(rolesRaw)}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Concerns bound on any platform's pre_tool_use slot — the undroppable set.
|
|
345
|
+
const guardBound = new Set<string>();
|
|
346
|
+
const platformsRaw = manifest["platforms"];
|
|
347
|
+
if (isObject(platformsRaw)) {
|
|
348
|
+
for (const block of Object.values(platformsRaw)) {
|
|
349
|
+
if (!isObject(block)) continue;
|
|
350
|
+
const pre = block["pre_tool_use"];
|
|
351
|
+
if (Array.isArray(pre)) {
|
|
352
|
+
for (const n of pre) {
|
|
353
|
+
if (typeof n === "string") guardBound.add(n);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const [role, spec] of Object.entries(rolesRaw)) {
|
|
359
|
+
if (!isObject(spec)) {
|
|
360
|
+
errors.push(`roles.${role}: must be a mapping, got ${pyTypeName(spec)}`);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const drop = spec["drop"];
|
|
364
|
+
if (drop === undefined || drop === null) {
|
|
365
|
+
continue; // a role without a drop list is a no-op entry — allowed
|
|
366
|
+
}
|
|
367
|
+
if (!Array.isArray(drop)) {
|
|
368
|
+
errors.push(`roles.${role}: 'drop' must be a list, got ${pyTypeName(drop)}`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
for (const name of drop) {
|
|
372
|
+
if (typeof name !== "string" || !concernNames.has(name)) {
|
|
373
|
+
errors.push(`roles.${role}: drop entry '${String(name)}' is not a known concern`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (guardBound.has(name)) {
|
|
377
|
+
errors.push(
|
|
378
|
+
`roles.${role}: drop entry '${name}' is bound to a pre_tool_use slot — ` +
|
|
379
|
+
`safety guards are undroppable on every role`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
// Severity-based twin of the slot check (review hardening 2026-08-10):
|
|
383
|
+
// a fail_closed or non-advisory concern is guard-shaped regardless of
|
|
384
|
+
// which slot it binds to — dropping one from a role chain would be a
|
|
385
|
+
// safety removal wearing an economy label.
|
|
386
|
+
const concernsRaw = manifest["concerns"];
|
|
387
|
+
const spec = isObject(concernsRaw) ? concernsRaw[name] : undefined;
|
|
388
|
+
if (isObject(spec)) {
|
|
389
|
+
const advisory = String(spec["severity"] ?? "").trim().toLowerCase() === "advisory";
|
|
390
|
+
const failClosed = spec["fail_closed"] === true;
|
|
391
|
+
if (failClosed || !advisory) {
|
|
392
|
+
errors.push(
|
|
393
|
+
`roles.${role}: drop entry '${name}' is ${failClosed ? "fail_closed" : "not severity: advisory"} — ` +
|
|
394
|
+
`only advisory, fail-open concerns may be role-dropped`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
321
402
|
export function lint(manifestPath: string, strict: boolean): number {
|
|
322
403
|
if (!_isFile(manifestPath)) {
|
|
323
404
|
process.stderr.write(
|
|
@@ -342,6 +423,7 @@ export function lint(manifestPath: string, strict: boolean): number {
|
|
|
342
423
|
const warnings: string[] = [];
|
|
343
424
|
const concernNames = _check_concerns(manifest, errors);
|
|
344
425
|
const bound = _check_platforms(manifest, concernNames, errors, warnings);
|
|
426
|
+
_check_roles(manifest, concernNames, errors);
|
|
345
427
|
_check_aliases(manifest, errors);
|
|
346
428
|
_check_orphan_trampolines(manifest, errors);
|
|
347
429
|
_check_dead_concerns(concernNames, bound, warnings);
|
|
@@ -22,6 +22,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
24
24
|
|
|
25
|
+
// ledger-exempt: single-artefact prose scan — the whole corpus is the one file README.md, and every hit is a line offset inside it that the failure output already prints in full; there is no enumerated target collection whose members could be silently continued past, and the file-absent / wrong-cwd case is a hard failure via assertWatchlistResolves rather than a green run over zero targets.
|
|
25
26
|
const _HERE = fileURLToPath(import.meta.url);
|
|
26
27
|
|
|
27
28
|
const README = 'README.md';
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `enforced_by`-declaration ratchet — a NEW rule states its enforcement, or
|
|
4
|
+
* states the gap; only the legacy set may stay silent.
|
|
5
|
+
*
|
|
6
|
+
* The enforcement-coverage number moved backwards across 9.23→9.29 (13.5% →
|
|
7
|
+
* 12.9%) for exactly one reason: rules were added faster than backstops, and
|
|
8
|
+
* nothing required a new rule to even *declare* where it stands. Coverage
|
|
9
|
+
* itself cannot be gated (whether a backstop is buildable is a per-rule
|
|
10
|
+
* judgment), but the *declaration* can: `enforced_by:` accepts an honest
|
|
11
|
+
* `none`, so requiring the key costs a new rule's author one line of truth,
|
|
12
|
+
* never a mechanism.
|
|
13
|
+
*
|
|
14
|
+
* Ratchet shape (the established baseline-the-legacy pattern):
|
|
15
|
+
* - The 84 rules that predate this gate are recorded in
|
|
16
|
+
* `src/config/rule-enforcement-baseline.json`. They stay legal while
|
|
17
|
+
* undeclared; the list may only shrink.
|
|
18
|
+
* - A rule NOT in the baseline must carry `enforced_by:` — a validator/
|
|
19
|
+
* hook/test/observer reference, or the explicit `none`.
|
|
20
|
+
* - A rule declaring `none` must also state the gap in its body (the
|
|
21
|
+
* `enforced_by: none` honesty convention the existing none-rules use) —
|
|
22
|
+
* a frontmatter-only `none` nobody reads is a silent gap again.
|
|
23
|
+
* - A baseline entry whose rule gained a declaration (or was deleted) must
|
|
24
|
+
* leave the baseline in the same change — a stale entry is a freed slot
|
|
25
|
+
* a later silent rule could hide in.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately NOT checked here: whether a declared validator resolves, is
|
|
28
|
+
* wired, or can fail a build — that is `check_enforcement_coverage`'s job
|
|
29
|
+
* (resolution), and `check_backstop_debt`'s (red backstops). This gate owns
|
|
30
|
+
* the declaration boundary only, on `src/rules/` only — gate scripts and
|
|
31
|
+
* their `// ledger-exempt:` markers are a different surface
|
|
32
|
+
* (`check_gate_completeness`) and are not read here.
|
|
33
|
+
*
|
|
34
|
+
* **Where the baseline lives, and why it ships.** `src/config/` is a directory
|
|
35
|
+
* entry in `package.json`'s `files[]`, so `rule-enforcement-baseline.json` is in
|
|
36
|
+
* the published tarball alongside its nine siblings — `gate-violation-baselines`,
|
|
37
|
+
* the four `*-budget` files, and the rest. All ten are repo-internal gate state:
|
|
38
|
+
* this gate is their consumer, it ships too (via `src/scripts/`), and in a
|
|
39
|
+
* consumer checkout it finds no `src/rules/` and does nothing. That is inert, not
|
|
40
|
+
* orphaned, and it is the pre-existing shape rather than something this file
|
|
41
|
+
* introduced — an automated review flagged it as "ships into dist with no
|
|
42
|
+
* consumer", which is doubly wrong: the file is not in `dist/` at all, and the
|
|
43
|
+
* consumer is this script. Whether ten repo-internal baselines belong in the
|
|
44
|
+
* tarball is a real packaging question; it is one decision about ten files, not a
|
|
45
|
+
* carve-out for the tenth.
|
|
46
|
+
*
|
|
47
|
+
* CLI contract: exit 0 = clean, 1 = a finding (or dead scan root), 2 = usage.
|
|
48
|
+
* `--quiet` mutes the clean-path line; `--root <dir>` re-roots for fixtures;
|
|
49
|
+
* `--write-baseline` records the current undeclared set (bootstrap/re-anchor —
|
|
50
|
+
* a deliberate maintainer act, never CI).
|
|
51
|
+
*/
|
|
52
|
+
import * as fs from 'node:fs';
|
|
53
|
+
import * as os from 'node:os';
|
|
54
|
+
import * as path from 'node:path';
|
|
55
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
56
|
+
|
|
57
|
+
import { read_frontmatter } from './check_enforcement_coverage.js';
|
|
58
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
59
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
60
|
+
import { is_kernel_rule } from './_lib/kernel_rules.js';
|
|
61
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
62
|
+
|
|
63
|
+
const GATE = 'lint_rule_enforcement_declaration';
|
|
64
|
+
|
|
65
|
+
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
66
|
+
|
|
67
|
+
const _HERE = path.resolve(fileURLToPath(import.meta.url));
|
|
68
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
69
|
+
|
|
70
|
+
const RULES_RELATIVE = path.join('src', 'rules');
|
|
71
|
+
const BASELINE_RELATIVE = path.join('src', 'config', 'rule-enforcement-baseline.json');
|
|
72
|
+
|
|
73
|
+
function _rootOverride(argv: readonly string[]): string | null {
|
|
74
|
+
const i = argv.indexOf('--root');
|
|
75
|
+
const value = i === -1 ? undefined : argv[i + 1];
|
|
76
|
+
return value === undefined ? null : path.resolve(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** enforced_by as declared, normalised to a string array; null = absent. */
|
|
80
|
+
function _declaration(fm: Record<string, string | string[]>): string[] | null {
|
|
81
|
+
const raw = fm['enforced_by'];
|
|
82
|
+
if (raw === undefined) return null;
|
|
83
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
84
|
+
return list.length === 0 ? null : list;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The body must state a `none` gap in prose — the honesty convention. */
|
|
88
|
+
function _bodyStatesNone(text: string): boolean {
|
|
89
|
+
const end = text.indexOf('\n---\n', 4);
|
|
90
|
+
const body = end === -1 ? text : text.slice(end + 5);
|
|
91
|
+
return body.includes('enforced_by: none') || body.includes('`enforced_by: none`');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _readBaseline(root: string): string[] | null {
|
|
95
|
+
try {
|
|
96
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(root, BASELINE_RELATIVE), 'utf-8'));
|
|
97
|
+
if (!Array.isArray(parsed) || !parsed.every((e) => typeof e === 'string')) return null;
|
|
98
|
+
return parsed as string[];
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Findings {
|
|
105
|
+
findings: string[];
|
|
106
|
+
undeclared: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _scan(root: string, baseline: ReadonlySet<string>, ledger: GateLedger | null): Findings {
|
|
110
|
+
const rulesDir = path.join(root, RULES_RELATIVE);
|
|
111
|
+
const files = fs
|
|
112
|
+
.readdirSync(rulesDir)
|
|
113
|
+
.filter((f) => f.endsWith('.md'))
|
|
114
|
+
.sort();
|
|
115
|
+
ledger?.plan(files);
|
|
116
|
+
|
|
117
|
+
const findings: string[] = [];
|
|
118
|
+
const undeclared: string[] = [];
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
seen.add(file);
|
|
123
|
+
const text = fs.readFileSync(path.join(rulesDir, file), 'utf-8');
|
|
124
|
+
const decl = _declaration(read_frontmatter(text));
|
|
125
|
+
|
|
126
|
+
if (decl === null) {
|
|
127
|
+
undeclared.push(file);
|
|
128
|
+
if (baseline.has(file)) {
|
|
129
|
+
ledger?.outOfScope(file, 'declared_exemption');
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const f =
|
|
133
|
+
`src/rules/${file} declares no \`enforced_by:\`. A new rule states its enforcement ` +
|
|
134
|
+
`(validator:/hook:/test:/observer:) or states the gap (\`none\` + a body line saying so). ` +
|
|
135
|
+
`The baseline covers only rules that predate this gate.`;
|
|
136
|
+
findings.push(f);
|
|
137
|
+
ledger?.fail(file, f);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Kernel rules are exempt from the body-prose convention, derived from
|
|
142
|
+
// _lib/kernel_rules.ts (never a hardcoded list) so the exemption closes
|
|
143
|
+
// itself the moment a rule leaves the kernel — the same self-closing
|
|
144
|
+
// shape validate_frontmatter uses for obligation_frequency. Their
|
|
145
|
+
// projected bytes are stability-gated, so requiring a prose line here
|
|
146
|
+
// would force an un-editable edit.
|
|
147
|
+
if (decl.includes('none') && !_bodyStatesNone(text) && !is_kernel_rule(file)) {
|
|
148
|
+
const f =
|
|
149
|
+
`src/rules/${file} declares \`enforced_by: none\` in frontmatter but never states the gap ` +
|
|
150
|
+
`in its body — the honesty convention is one prose line naming \`enforced_by: none\`, so a ` +
|
|
151
|
+
`reader of the rule sees the gap, not only a machine.`;
|
|
152
|
+
findings.push(f);
|
|
153
|
+
ledger?.fail(file, f);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ledger?.complete(file);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const entry of [...baseline].sort()) {
|
|
161
|
+
if (!seen.has(entry)) {
|
|
162
|
+
findings.push(
|
|
163
|
+
`${BASELINE_RELATIVE} baseline entry \`${entry}\` names no existing rule — remove it in the ` +
|
|
164
|
+
`same change that renamed/deleted the rule. A stale entry is a freed slot a later ` +
|
|
165
|
+
`undeclared rule could silently occupy.`,
|
|
166
|
+
);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!undeclared.includes(entry)) {
|
|
170
|
+
findings.push(
|
|
171
|
+
`${BASELINE_RELATIVE} baseline entry \`${entry}\` now declares \`enforced_by:\` — remove it ` +
|
|
172
|
+
`from the baseline in the same change. The list may only shrink.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { findings, undeclared };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Prove against the real CLI that the ratchet rejects what it must. */
|
|
181
|
+
export function selfTest(): number {
|
|
182
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lred-selftest-'));
|
|
183
|
+
const declared = '---\ntype: "auto"\nenforced_by:\n - "validator:src/scripts/example.ts"\n---\n\n# R\n\nBody.\n';
|
|
184
|
+
const noneHonest =
|
|
185
|
+
'---\ntype: "auto"\nenforced_by:\n - "none"\n---\n\n# R\n\nThis rule ships `enforced_by: none` — the obligation is model-carried.\n';
|
|
186
|
+
const noneSilent = '---\ntype: "auto"\nenforced_by:\n - "none"\n---\n\n# R\n\nBody with no gap statement.\n';
|
|
187
|
+
const silent = '---\ntype: "auto"\n---\n\n# R\n\nBody.\n';
|
|
188
|
+
|
|
189
|
+
const seed = (name: string, rules: Record<string, string>, baseline: string[]): string => {
|
|
190
|
+
const root = path.join(tmp, name);
|
|
191
|
+
fs.mkdirSync(path.join(root, RULES_RELATIVE), { recursive: true });
|
|
192
|
+
fs.mkdirSync(path.join(root, 'src', 'config'), { recursive: true });
|
|
193
|
+
for (const [file, text] of Object.entries(rules)) {
|
|
194
|
+
fs.writeFileSync(path.join(root, RULES_RELATIVE, file), text, 'utf-8');
|
|
195
|
+
}
|
|
196
|
+
fs.writeFileSync(path.join(root, BASELINE_RELATIVE), `${JSON.stringify(baseline, null, 2)}\n`, 'utf-8');
|
|
197
|
+
return root;
|
|
198
|
+
};
|
|
199
|
+
const run = (cwd: string): number =>
|
|
200
|
+
runGateCli(REPO_ROOT, `src/scripts/${GATE}.ts`, ['--quiet', '--root', cwd], REPO_ROOT);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
return runSelfTest({
|
|
204
|
+
gate: GATE,
|
|
205
|
+
minCases: 6,
|
|
206
|
+
minRejectCases: 4,
|
|
207
|
+
cases: [
|
|
208
|
+
{
|
|
209
|
+
name: 'a declared new rule and a baselined legacy rule pass',
|
|
210
|
+
expect: 'accept',
|
|
211
|
+
run: () => run(seed('clean', { 'a.md': declared, 'legacy.md': silent }, ['legacy.md'])),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'an honest none (frontmatter + body gap line) passes',
|
|
215
|
+
expect: 'accept',
|
|
216
|
+
run: () => run(seed('none-honest', { 'a.md': noneHonest }, [])),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'a NEW rule without enforced_by is rejected',
|
|
220
|
+
expect: 'reject',
|
|
221
|
+
run: () => run(seed('new-silent', { 'a.md': silent }, [])),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'a none declaration without the body gap line is rejected',
|
|
225
|
+
expect: 'reject',
|
|
226
|
+
run: () => run(seed('none-silent', { 'a.md': noneSilent }, [])),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'a stale baseline entry (rule deleted/renamed) is rejected',
|
|
230
|
+
expect: 'reject',
|
|
231
|
+
run: () => run(seed('stale', { 'a.md': declared }, ['gone.md'])),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'a baseline entry whose rule gained a declaration is rejected until removed',
|
|
235
|
+
expect: 'reject',
|
|
236
|
+
run: () => run(seed('shrink', { 'a.md': declared }, ['a.md'])),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'a dead scan root is rejected, never passed as nothing-to-check',
|
|
240
|
+
expect: 'reject',
|
|
241
|
+
run: () => run(path.join(tmp, 'dead-root-does-not-exist')),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
} finally {
|
|
246
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function main(): number {
|
|
251
|
+
const argv = process.argv.slice(2);
|
|
252
|
+
if (argv.includes('--self-test')) {
|
|
253
|
+
return selfTest();
|
|
254
|
+
}
|
|
255
|
+
const root = _rootOverride(argv) ?? REPO_ROOT;
|
|
256
|
+
|
|
257
|
+
const rulesDir = path.join(root, RULES_RELATIVE);
|
|
258
|
+
let isDir = false;
|
|
259
|
+
try {
|
|
260
|
+
isDir = fs.statSync(rulesDir).isDirectory();
|
|
261
|
+
} catch {
|
|
262
|
+
isDir = false;
|
|
263
|
+
}
|
|
264
|
+
if (!isDir) {
|
|
265
|
+
process.stderr.write(`❌ ${GATE}: rules dir not found at ${RULES_RELATIVE} — the scan root is dead.\n`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (argv.includes('--write-baseline')) {
|
|
270
|
+
const { undeclared } = _scan(root, new Set(), null);
|
|
271
|
+
fs.writeFileSync(
|
|
272
|
+
path.join(root, BASELINE_RELATIVE),
|
|
273
|
+
`${JSON.stringify(undeclared, null, 2)}\n`,
|
|
274
|
+
'utf-8',
|
|
275
|
+
);
|
|
276
|
+
process.stdout.write(`${GATE}: baseline written — ${undeclared.length} legacy undeclared rule(s).\n`);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const baseline = _readBaseline(root);
|
|
281
|
+
if (baseline === null) {
|
|
282
|
+
process.stderr.write(
|
|
283
|
+
`❌ ${GATE}: ${BASELINE_RELATIVE} missing or not a string array — run --write-baseline once ` +
|
|
284
|
+
`(a deliberate maintainer act) or restore the committed file.\n`,
|
|
285
|
+
);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const ledger = new GateLedger(GATE);
|
|
290
|
+
const { findings } = _scan(root, new Set(baseline), ledger);
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
reportScanned({
|
|
294
|
+
gate: GATE,
|
|
295
|
+
scanned: fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md')).length,
|
|
296
|
+
units: 'rule file(s)',
|
|
297
|
+
roots: [RULES_RELATIVE],
|
|
298
|
+
});
|
|
299
|
+
} catch (e) {
|
|
300
|
+
if (e instanceof DeadScopeError) {
|
|
301
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
throw e;
|
|
305
|
+
}
|
|
306
|
+
ledger.report();
|
|
307
|
+
|
|
308
|
+
if (findings.length > 0) {
|
|
309
|
+
for (const f of findings) {
|
|
310
|
+
process.stderr.write(`❌ ${f}\n`);
|
|
311
|
+
}
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
if (!QUIET) {
|
|
315
|
+
process.stdout.write(
|
|
316
|
+
`✅ ${GATE}: every non-baselined rule declares its enforcement (baseline: ${baseline.length}, shrink-only).\n`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
323
|
+
function _isCliEntry(): boolean {
|
|
324
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (process.argv[1] === undefined) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
331
|
+
if (import.meta.url === argvUrl) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
336
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
337
|
+
return here === argv1;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (_isCliEntry()) {
|
|
344
|
+
process.exit(main());
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export { GATE, REPO_ROOT, BASELINE_RELATIVE, main };
|