@event4u/agent-config 9.17.0 → 9.18.1
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 +122 -171
- package/dist/agent-src/commands/challenge-me/vision.md +5 -0
- package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
- package/dist/agent-src/commands/feature/plan.md +20 -0
- package/dist/agent-src/commands/feature/roadmap.md +19 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/roadmap/create.md +29 -0
- package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/architecture.md +2 -0
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/brand-consistency.md +10 -62
- package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/cli-output-handling.md +5 -0
- package/dist/agent-src/rules/code-comment-discipline.md +3 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/design-fidelity.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +4 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/laravel-routing.md +4 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +5 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +3 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +4 -0
- package/dist/agent-src/rules/source-discovery-gate.md +3 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +5 -0
- package/dist/agent-src/rules/ui-audit-gate.md +2 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
- package/dist/agent-src/skills/design-review/SKILL.md +4 -141
- package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
- package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
- package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
- package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +12 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
- package/dist/cli-delegate/{chunk-IMSXPRVH.js → chunk-ELMFOQPW.js} +11 -0
- package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
- package/dist/cli-delegate/cmd_explain.js +36 -36
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +203 -2228
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +249 -0
- package/dist/cli-delegate/cmd_route_explain.js +285 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +57 -57
- 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/install/install.mjs +11 -0
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +12 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/src/config/agent-settings.template.yml +46 -0
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +44 -0
- package/src/scripts/_cli/cmd_explain.ts +41 -36
- package/src/scripts/_cli/cmd_route_audit.ts +290 -0
- package/src/scripts/_cli/cmd_route_explain.ts +269 -0
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/git_env.ts +40 -0
- package/src/scripts/_lib/md_table.ts +27 -0
- package/src/scripts/_lib/planning_settings.ts +43 -0
- package/src/scripts/_lib/release_highlights.ts +279 -0
- package/src/scripts/_lib/router_match.ts +301 -0
- package/src/scripts/annotate_r1_outcomes.ts +258 -0
- package/src/scripts/check_completion_review.ts +1322 -0
- package/src/scripts/check_release_highlights.ts +60 -152
- package/src/scripts/compile_router.ts +20 -3
- package/src/scripts/dispatch_r2_reviewer.ts +969 -0
- package/src/scripts/eval_ui_triviality.ts +156 -0
- package/src/scripts/lint_abstraction_thresholds.ts +3 -1
- package/src/scripts/lint_plan_risk_register.ts +931 -0
- package/src/scripts/lint_trigger_collisions.ts +257 -0
- package/src/scripts/release.ts +47 -13
- package/src/scripts/router_telemetry.ts +16 -290
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/server/schemas/settings.ts +18 -0
|
@@ -550,6 +550,52 @@ roadmap:
|
|
|
550
550
|
# only if your team genuinely commits work on a calendar plate.
|
|
551
551
|
horizon_weeks: 0
|
|
552
552
|
|
|
553
|
+
# --- Plan governance gates ---
|
|
554
|
+
#
|
|
555
|
+
# Three governance gates around the plan/delivery workflow (contract:
|
|
556
|
+
# docs/contracts/plan-review-gates.md). Scope is every plan artifact —
|
|
557
|
+
# roadmaps AND feature plans — which is why these keys do not live under
|
|
558
|
+
# `roadmap:`. All three default to true; a missing key behaves as true.
|
|
559
|
+
planning:
|
|
560
|
+
# Gate C — plan-confidence gate, BEFORE authoring (true, false).
|
|
561
|
+
# true = default. A plan-artifact authoring ask (/roadmap:create,
|
|
562
|
+
# roadmap-writing, /feature:plan, /feature:roadmap) first runs
|
|
563
|
+
# the four 95%-confidence conditions from /challenge-me vision.
|
|
564
|
+
# All four hold → the agent creates directly and emits exactly
|
|
565
|
+
# one marker line ("Confidence ≥ 95% — creating directly").
|
|
566
|
+
# Any gap → the ask routes into the /challenge-me vision
|
|
567
|
+
# interview first; hosts without that command run the inline
|
|
568
|
+
# degrade interview (one question per turn, same stop
|
|
569
|
+
# conditions — contexts/execution/plan-confidence-gate.md).
|
|
570
|
+
# An explicit user bypass ("just write it") wins for that turn
|
|
571
|
+
# and is counted (gate_c_bypass_rate).
|
|
572
|
+
# false = inert; plan asks author directly (today's behavior).
|
|
573
|
+
challenge_on_create: true
|
|
574
|
+
|
|
575
|
+
# Gate R1 — plan-risk review at plan creation / substantial change
|
|
576
|
+
# (true, false).
|
|
577
|
+
# true = default. Every ready (non-draft) plan carries a schema-valid
|
|
578
|
+
# `## Risk Register` section (ranked risks, mitigation +
|
|
579
|
+
# in-document anchor per row, freshness marker). Enforced by
|
|
580
|
+
# lint_plan_risk_register at pre-push + CI; drafts are exempt
|
|
581
|
+
# until flipped to ready. Honest-null: an empty register is
|
|
582
|
+
# valid only with the exact marker grammar from the contract.
|
|
583
|
+
# false = escape hatch; the validator skips (governance default is on).
|
|
584
|
+
risk_review: true
|
|
585
|
+
|
|
586
|
+
# Gate R2 — completion review at 100% roadmap completion / pre-PR
|
|
587
|
+
# (true, false).
|
|
588
|
+
# true = default. A findings-BEFORE-fixes review by a fresh reviewer
|
|
589
|
+
# context: the findings artifact (agents/evidence/reviews/…) must exist
|
|
590
|
+
# for the current diff hash before the first fix commit; every
|
|
591
|
+
# finding ends fixed | accepted-risk | deferred. No code
|
|
592
|
+
# surface → explicit skip declaration, never a silent skip.
|
|
593
|
+
# Enforced by check_completion_review at pre-push + CI (CI
|
|
594
|
+
# authoritative); a crashed validator (exit 2) warns and
|
|
595
|
+
# allows — a broken gate never blocks its own fix.
|
|
596
|
+
# false = escape hatch; the validator skips (governance default is on).
|
|
597
|
+
completion_review: true
|
|
598
|
+
|
|
553
599
|
# --- Quality / CI execution ---
|
|
554
600
|
#
|
|
555
601
|
# Controls whether the agent runs the project's quality pipeline
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"deterministic": false
|
|
59
59
|
},
|
|
60
60
|
"cli_help_command_count": {
|
|
61
|
-
"max":
|
|
62
|
-
"last_measured":
|
|
61
|
+
"max": 84,
|
|
62
|
+
"last_measured": 84,
|
|
63
63
|
"last_measured_at": "2026-08-04",
|
|
64
64
|
"method": "registry enumeration — count of `{ name: '...', disposition` entries in src/cli/registry.ts (NOT --help prose parsing; methods historically disagreed 74 vs 76 vs 79)",
|
|
65
|
-
"baseline_note": "80 → 81 on 2026-08-03 (release 9.15.0 gate). This RECORDS growth that already shipped through its own reviewed PR; it does not authorize new surface. The +1 is f02db54c2 (fix(cli): register routing:doctor in the CLI registry) — the command existed, only its registry entry was missing, and the registering PR did not move this budget. The gate did its job again: breach WARNS on main and BLOCKS on release, which is the conscious-decision point rather than a silently raised cap. Re-measured 2026-08-03 via the pinned method: 81. Headroom stays zero by design — the next registry addition must move this number in its own PR. 81 → 82 on 2026-08-04: the `handoff` subcommand (road-to-agent-handoff-resume Phase 4) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 82."
|
|
65
|
+
"baseline_note": "80 → 81 on 2026-08-03 (release 9.15.0 gate). This RECORDS growth that already shipped through its own reviewed PR; it does not authorize new surface. The +1 is f02db54c2 (fix(cli): register routing:doctor in the CLI registry) — the command existed, only its registry entry was missing, and the registering PR did not move this budget. The gate did its job again: breach WARNS on main and BLOCKS on release, which is the conscious-decision point rather than a silently raised cap. Re-measured 2026-08-03 via the pinned method: 81. Headroom stays zero by design — the next registry addition must move this number in its own PR. 81 → 82 on 2026-08-04: the `handoff` subcommand (road-to-agent-handoff-resume Phase 4) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 82. 82 → 84 on 2026-08-04: the `route:explain` + `route:audit` verbs (routing-correctness validation surface) register in the same PR that adds the surface. Re-measured via the pinned method: 84."
|
|
66
66
|
},
|
|
67
67
|
"mcp_public_tool_count": {
|
|
68
68
|
"max": 19,
|
|
@@ -84,6 +84,12 @@
|
|
|
84
84
|
# contract is `dist == rewrite(src)`; `check_ci_local_parity`, whose corpus is
|
|
85
85
|
# workflow files) carries no recipe and is reported UNPROVEN, never green.
|
|
86
86
|
gates:
|
|
87
|
+
- id: lint_trigger_collisions
|
|
88
|
+
argv: []
|
|
89
|
+
min_scanned: 90
|
|
90
|
+
corpus: "src/rules/*.md frontmatters (111 at baseline)"
|
|
91
|
+
status: enforced
|
|
92
|
+
|
|
87
93
|
- id: check_condensation
|
|
88
94
|
argv: []
|
|
89
95
|
min_scanned: 380
|
|
@@ -302,3 +308,41 @@ gates:
|
|
|
302
308
|
count. Its inventory-file exemption was also dead: it compared against
|
|
303
309
|
the retired root AND against 'dist/agent-src' as a single path segment,
|
|
304
310
|
which a '/'-split can never yield.
|
|
311
|
+
|
|
312
|
+
- id: lint_plan_risk_register
|
|
313
|
+
argv: []
|
|
314
|
+
min_scanned: 12
|
|
315
|
+
corpus: "ready roadmap files under agents/roadmaps/ top level (19 at baseline: 14 grandfathered + 4 draft-exempt + 1 carrying a live register)"
|
|
316
|
+
status: enforced
|
|
317
|
+
note: >
|
|
318
|
+
Gate R1 (docs/contracts/plan-review-gates.md § 1). The count is READY
|
|
319
|
+
FILES INSPECTED incl. grandfathered/draft-exempt ones — exemptions are
|
|
320
|
+
reported, never silently skipped, so a collapse below the floor means
|
|
321
|
+
the roadmaps root moved, not that the corpus went clean.
|
|
322
|
+
|
|
323
|
+
- id: check_completion_review
|
|
324
|
+
argv: ["--advisory", "--base", "origin/main"]
|
|
325
|
+
min_scanned: 1
|
|
326
|
+
corpus: "findings artefacts under agents/evidence/reviews/ + the current-diff evaluation (1 at baseline — the evaluation itself; artefact count grows with gated PRs)"
|
|
327
|
+
status: enforced
|
|
328
|
+
note: >
|
|
329
|
+
Gate R2 (docs/contracts/plan-review-gates.md § 2). HONEST LIMIT, stated
|
|
330
|
+
because a floor that cannot fail is exactly the false green this file
|
|
331
|
+
exists to prevent: scanned = artefacts inspected + 1 for the diff
|
|
332
|
+
evaluation, so on any run that REACHES the count `min_scanned: 1` cannot
|
|
333
|
+
trip. It is not unreachable in principle — a moved reviews root exits
|
|
334
|
+
before the inventory and reports `scanned: 0`, which the guard does read —
|
|
335
|
+
but that path is the dead-scope violation below, so the floor never adds
|
|
336
|
+
anything the assertion does not already catch.
|
|
337
|
+
NOTHING TRIPS AT ALL DURING THE ADVISORY WINDOW. The argv above
|
|
338
|
+
mirrors the CI invocation, and under `--advisory` every violation kind —
|
|
339
|
+
dead-scan-scope included — is printed as a warning and the exit code is 0.
|
|
340
|
+
So for the duration of the Stage-A window this entry proves only that the
|
|
341
|
+
gate RAN: it has no blocking path and no trippable floor. The blocking
|
|
342
|
+
behaviour is real but dormant — the dead-scan-scope assertion is a policy
|
|
343
|
+
violation, exit 1 (contract § 6 carve-out: a gate that read nothing has not
|
|
344
|
+
passed), which is what makes a moved reviews root fail loudly instead of
|
|
345
|
+
reporting a clean scope. It takes effect when Stage B drops `--advisory`
|
|
346
|
+
from the CI invocation; nothing else changes at that switch. The entry
|
|
347
|
+
stays registered so the guard reports this gate rather than skipping it
|
|
348
|
+
silently.
|
|
@@ -61,6 +61,8 @@ import {
|
|
|
61
61
|
load_agent_settings,
|
|
62
62
|
resolve_project_root,
|
|
63
63
|
} from '../_lib/agent_settings.js';
|
|
64
|
+
import { trigger_matches } from '../_lib/router_match.js';
|
|
65
|
+
import type { Trigger } from '../_lib/router_match.js';
|
|
64
66
|
import * as presets from '../config/presets.js';
|
|
65
67
|
import * as profiles from '../config/profiles.js';
|
|
66
68
|
import { build_trace } from './explain_last/index.js';
|
|
@@ -410,52 +412,51 @@ function pyReprAny(value: unknown): string {
|
|
|
410
412
|
return String(value);
|
|
411
413
|
}
|
|
412
414
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
return
|
|
415
|
+
/** `kind: value` label for a trigger's single match key. */
|
|
416
|
+
function _trigger_reason(trigger: Dict): string {
|
|
417
|
+
for (const kind of ['keyword', 'phrase', 'command', 'path_prefix', 'file_pattern']) {
|
|
418
|
+
if (kind in trigger) {
|
|
419
|
+
return `${kind}: ${String(trigger[kind])}`;
|
|
418
420
|
}
|
|
419
421
|
}
|
|
420
|
-
|
|
421
|
-
const ph = String(trigger['phrase']).toLowerCase();
|
|
422
|
-
if (ph && lowered.includes(ph)) {
|
|
423
|
-
return `phrase: ${ph}`;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if ('path_prefix' in trigger) {
|
|
427
|
-
const prefix = String(trigger['path_prefix']);
|
|
428
|
-
if (prefix && text.includes(prefix)) {
|
|
429
|
-
return `path_prefix: ${prefix}`;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return null;
|
|
422
|
+
return `trigger: ${JSON.stringify(trigger)}`;
|
|
433
423
|
}
|
|
434
424
|
|
|
435
425
|
function _explain_route(project_root: string, text: string, as_json: boolean): number {
|
|
436
426
|
const router = _load_router(project_root);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
427
|
+
// Matching goes through the SHARED matcher (_lib/router_match) — anchored
|
|
428
|
+
// keyword semantics, both tiers. This surface carries no open-files /
|
|
429
|
+
// command context, so path_prefix / file_pattern / command triggers
|
|
430
|
+
// cannot match here (previously path_prefix was probed against the
|
|
431
|
+
// prompt TEXT — a divergent second matcher, removed on purpose).
|
|
432
|
+
const tier_1_matches: Dict[] = [];
|
|
433
|
+
const tier_2_matches: Dict[] = [];
|
|
434
|
+
for (const [tier, sink] of [
|
|
435
|
+
['tier_1', tier_1_matches],
|
|
436
|
+
['tier_2', tier_2_matches],
|
|
437
|
+
] as Array<[string, Dict[]]>) {
|
|
438
|
+
const rules = (router[tier] as Dict[] | undefined) || [];
|
|
439
|
+
for (const entry of rules) {
|
|
440
|
+
const triggers = (entry['triggers'] as Dict[] | undefined) || [];
|
|
441
|
+
for (const trig of triggers) {
|
|
442
|
+
if (trigger_matches(trig as Trigger, text, null, null)) {
|
|
443
|
+
sink.push({ id: entry['id'], tier, reason: _trigger_reason(trig) });
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
447
446
|
}
|
|
448
447
|
}
|
|
449
448
|
}
|
|
449
|
+
const total = tier_1_matches.length + tier_2_matches.length;
|
|
450
450
|
const kernel_always = [...((router['kernel'] as unknown[] | undefined) || [])];
|
|
451
451
|
const payload: Dict = {
|
|
452
452
|
input: text,
|
|
453
453
|
kernel_always,
|
|
454
|
-
tier_1_matches
|
|
454
|
+
tier_1_matches,
|
|
455
|
+
tier_2_matches,
|
|
455
456
|
};
|
|
456
457
|
if (as_json) {
|
|
457
458
|
_dumpJson(payload);
|
|
458
|
-
return 0;
|
|
459
|
+
return total === 0 ? 1 : 0;
|
|
459
460
|
}
|
|
460
461
|
print(` input: ${pyReprStr(text)}`);
|
|
461
462
|
print();
|
|
@@ -464,14 +465,19 @@ function _explain_route(project_root: string, text: string, as_json: boolean): n
|
|
|
464
465
|
print(` · ${pyStr(kid)}`);
|
|
465
466
|
}
|
|
466
467
|
print();
|
|
467
|
-
print(` tier-1 matches (${
|
|
468
|
-
|
|
469
|
-
print(
|
|
470
|
-
return 1;
|
|
468
|
+
print(` tier-1 matches (${tier_1_matches.length}):`);
|
|
469
|
+
for (const match of tier_1_matches) {
|
|
470
|
+
print(` · ${match['id']} (${match['reason']})`);
|
|
471
471
|
}
|
|
472
|
-
|
|
472
|
+
print();
|
|
473
|
+
print(` tier-2 matches (${tier_2_matches.length}):`);
|
|
474
|
+
for (const match of tier_2_matches) {
|
|
473
475
|
print(` · ${match['id']} (${match['reason']})`);
|
|
474
476
|
}
|
|
477
|
+
if (total === 0) {
|
|
478
|
+
print(' · (no trigger matched — only kernel rules active)');
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
475
481
|
return 0;
|
|
476
482
|
}
|
|
477
483
|
|
|
@@ -757,7 +763,6 @@ export {
|
|
|
757
763
|
_explain_config,
|
|
758
764
|
_find_rule,
|
|
759
765
|
_explain_rule,
|
|
760
|
-
_matches_trigger,
|
|
761
766
|
_explain_route,
|
|
762
767
|
_explain_last,
|
|
763
768
|
_LAST_HELP,
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* `agent-config route:audit [--last N] [--record] [--weekly] [--json]` —
|
|
4
|
+
* replay the shared router matcher over the last N USER prompts of the
|
|
5
|
+
* repo-local chat-history log and render matched-vs-unmatched per prompt.
|
|
6
|
+
* Read-only over the inputs; no LLM call; deterministic.
|
|
7
|
+
*
|
|
8
|
+
* Measurement level (ADR-126): trigger matching only — what the host actually
|
|
9
|
+
* invoked on those turns is NOT measured here. "Should-have-matched" is the
|
|
10
|
+
* human reviewer's judgment over this table, not a computed verdict.
|
|
11
|
+
*
|
|
12
|
+
* `--record` appends one JSONL record per audited prompt to the opt-in
|
|
13
|
+
* routing recorder (`telemetry.routing_recorder.enabled`, default OFF —
|
|
14
|
+
* disabled means zero file IO and a silent exit contribution). The record is
|
|
15
|
+
* PII-excluded by construction: it carries a truncated sha256 of the prompt,
|
|
16
|
+
* never the prompt text, plus matched rule ids / trigger labels and an
|
|
17
|
+
* enforcement join against `agents/runtime/state/rule-trips.json` where a
|
|
18
|
+
* same-named hook concern exists. Delete-and-rerun rebuilds it from chat
|
|
19
|
+
* history — losing it changes no answer (state-store test, ADR-124 § 6).
|
|
20
|
+
*
|
|
21
|
+
* `--weekly` renders the rolling last-7-days picture from the recorder log.
|
|
22
|
+
* Resolver authority note: this instrument is corpus-growth raw material for
|
|
23
|
+
* `internal/bench/layer1-resolver-PREREG.md` (its P2 condition) as a side
|
|
24
|
+
* effect only — the PREREG stays the sole authority over any resolver
|
|
25
|
+
* revival; nothing here builds or scores a resolver.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 rendered (even when the recorder is disabled), 1 no prompts /
|
|
28
|
+
* no records to audit, 2 invocation or IO error.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as crypto from 'node:crypto';
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { match_prompt, type Router } from '../_lib/router_match.js';
|
|
37
|
+
import { load_agent_settings } from '../_lib/agent_settings.js';
|
|
38
|
+
import { read_entries } from '../chat_history.js';
|
|
39
|
+
|
|
40
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
41
|
+
const ROUTER_JSON = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
42
|
+
|
|
43
|
+
// cache-version: v1 — rebuildable audit log: delete-and-rerun rebuilds it from
|
|
44
|
+
// chat history (`route:audit --record`); losing it changes no answer.
|
|
45
|
+
export const RECORDER_REL = 'agents/runtime/state/routing-telemetry.jsonl';
|
|
46
|
+
const RULE_TRIPS_REL = 'agents/runtime/state/rule-trips.json';
|
|
47
|
+
export const RECORD_SCHEMA_VERSION = 1;
|
|
48
|
+
|
|
49
|
+
export const MEASUREMENT_HEADER =
|
|
50
|
+
'Measurement level: trigger matching only — what the host actually invokes is NOT measured here (ADR-126).';
|
|
51
|
+
export const PREREG_AUTHORITY_LINE =
|
|
52
|
+
'Resolver authority: internal/bench/layer1-resolver-PREREG.md — this corpus feeds its P2 condition as a side effect; nothing here revives a resolver.';
|
|
53
|
+
|
|
54
|
+
function recorder_path(): string {
|
|
55
|
+
return process.env['AGENT_ROUTING_TELEMETRY_FILE'] ?? path.join(REPO_ROOT, RECORDER_REL);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function recorder_enabled(): boolean {
|
|
59
|
+
try {
|
|
60
|
+
const settings = load_agent_settings({ cwd: REPO_ROOT }) as Record<string, unknown>;
|
|
61
|
+
const t = settings['telemetry'];
|
|
62
|
+
if (t === null || typeof t !== 'object' || Array.isArray(t)) return false;
|
|
63
|
+
const rr = (t as Record<string, unknown>)['routing_recorder'];
|
|
64
|
+
if (rr === null || typeof rr !== 'object' || Array.isArray(rr)) return false;
|
|
65
|
+
const enabled = (rr as Record<string, unknown>)['enabled'];
|
|
66
|
+
return enabled === true || enabled === 'true' || enabled === 'on' || enabled === 'yes';
|
|
67
|
+
} catch {
|
|
68
|
+
return false; // unparseable means disabled — default-off doctrine
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface AuditedPrompt {
|
|
73
|
+
prompt: string;
|
|
74
|
+
matched: { rule: string; tier: string; triggers: string[] }[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _trigger_label(trigger: Record<string, unknown>): string {
|
|
78
|
+
for (const kind of ['keyword', 'phrase', 'command', 'path_prefix', 'file_pattern']) {
|
|
79
|
+
if (kind in trigger) return `${kind}: ${String(trigger[kind])}`;
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(trigger);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function audit_prompts(router: Router, prompts: string[]): AuditedPrompt[] {
|
|
85
|
+
return prompts.map((prompt) => {
|
|
86
|
+
const result = match_prompt(router, prompt, 'full', null, null);
|
|
87
|
+
const byRule = new Map<string, { tier: string; triggers: string[] }>();
|
|
88
|
+
for (const mt of result.matched_triggers) {
|
|
89
|
+
const id = String(mt.rule);
|
|
90
|
+
const entry = byRule.get(id) ?? { tier: mt.tier.replace('_', '-'), triggers: [] };
|
|
91
|
+
entry.triggers.push(_trigger_label(mt.trigger as Record<string, unknown>));
|
|
92
|
+
byRule.set(id, entry);
|
|
93
|
+
}
|
|
94
|
+
const matched = [...byRule.entries()]
|
|
95
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
96
|
+
.map(([rule, v]) => ({ rule, tier: v.tier, triggers: v.triggers }));
|
|
97
|
+
return { prompt, matched };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Last N user prompts from the repo chat-history log, oldest first. */
|
|
102
|
+
export function load_user_prompts(last: number, history_path?: string | null): string[] {
|
|
103
|
+
let entries: Array<{ t?: unknown; text?: unknown }> = [];
|
|
104
|
+
try {
|
|
105
|
+
entries = read_entries({ path: history_path ?? null }) as Array<{ t?: unknown; text?: unknown }>;
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const prompts: string[] = [];
|
|
110
|
+
for (const e of entries) {
|
|
111
|
+
if (e === null || typeof e !== 'object') continue;
|
|
112
|
+
if (e.t !== 'user' || typeof e.text !== 'string') continue;
|
|
113
|
+
const text = e.text.trim();
|
|
114
|
+
// Host-injected meta turns are not user prompts (same filter as the
|
|
115
|
+
// handoff session reader).
|
|
116
|
+
if (text === '' || text.startsWith('<') || text.startsWith('Caveat:')) continue;
|
|
117
|
+
prompts.push(text);
|
|
118
|
+
}
|
|
119
|
+
return prompts.slice(-last);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface TripsDoc {
|
|
123
|
+
concerns?: Record<string, { block?: number; warn?: number }>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function _load_trips(): TripsDoc {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, RULE_TRIPS_REL), 'utf-8')) as TripsDoc;
|
|
129
|
+
} catch {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function build_records(audits: AuditedPrompt[], now_iso: string): Record<string, unknown>[] {
|
|
135
|
+
const trips = _load_trips();
|
|
136
|
+
return audits.map((a) => {
|
|
137
|
+
const enforcement_trips: Record<string, { block: number; warn: number }> = {};
|
|
138
|
+
for (const m of a.matched) {
|
|
139
|
+
const concern = trips.concerns?.[m.rule];
|
|
140
|
+
if (concern) {
|
|
141
|
+
enforcement_trips[m.rule] = { block: concern.block ?? 0, warn: concern.warn ?? 0 };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// PII-exclusion-by-construction: no free-form prompt field exists in
|
|
145
|
+
// this record type — only a truncated digest and closed-shape labels.
|
|
146
|
+
return {
|
|
147
|
+
schema_version: RECORD_SCHEMA_VERSION,
|
|
148
|
+
ts: now_iso,
|
|
149
|
+
prompt_sha16: crypto.createHash('sha256').update(a.prompt).digest('hex').slice(0, 16),
|
|
150
|
+
matched: a.matched,
|
|
151
|
+
enforcement_trips,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function append_records(records: Record<string, unknown>[], target: string): number {
|
|
157
|
+
if (process.env['AGENT_CONFIG_REPLAY'] === '1') return 0; // replay mode: no state writes
|
|
158
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
159
|
+
const fd = fs.openSync(target, 'a', 0o644);
|
|
160
|
+
try {
|
|
161
|
+
for (const r of records) fs.writeSync(fd, JSON.stringify(r) + '\n');
|
|
162
|
+
fs.fsyncSync(fd);
|
|
163
|
+
} finally {
|
|
164
|
+
fs.closeSync(fd);
|
|
165
|
+
}
|
|
166
|
+
return records.length;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function render_weekly(target: string, now: Date): { text: string; records: number } {
|
|
170
|
+
let lines: string[] = [];
|
|
171
|
+
try {
|
|
172
|
+
lines = fs.readFileSync(target, 'utf-8').split('\n').filter(Boolean);
|
|
173
|
+
} catch {
|
|
174
|
+
lines = [];
|
|
175
|
+
}
|
|
176
|
+
const cutoff = now.getTime() - 7 * 24 * 60 * 60 * 1000;
|
|
177
|
+
const perRule = new Map<string, number>();
|
|
178
|
+
let inWindow = 0;
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
let rec: { ts?: string; matched?: { rule: string }[] };
|
|
181
|
+
try {
|
|
182
|
+
rec = JSON.parse(line) as typeof rec;
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const ts = Date.parse(rec.ts ?? '');
|
|
187
|
+
if (Number.isNaN(ts) || ts < cutoff) continue;
|
|
188
|
+
inWindow += 1;
|
|
189
|
+
for (const m of rec.matched ?? []) {
|
|
190
|
+
perRule.set(m.rule, (perRule.get(m.rule) ?? 0) + 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const out: string[] = [MEASUREMENT_HEADER, ''];
|
|
194
|
+
out.push(`routing recorder — rolling 7-day window (${inWindow} recorded prompt(s))`);
|
|
195
|
+
if (inWindow === 0) {
|
|
196
|
+
out.push(' (no records in the window — enable telemetry.routing_recorder and run `route:audit --record`)');
|
|
197
|
+
} else {
|
|
198
|
+
const rows = [...perRule.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
199
|
+
for (const [rule, n] of rows) out.push(` ${String(n).padStart(4)} × ${rule}`);
|
|
200
|
+
}
|
|
201
|
+
out.push('');
|
|
202
|
+
out.push(PREREG_AUTHORITY_LINE);
|
|
203
|
+
return { text: out.join('\n') + '\n', records: inWindow };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function main(argv: string[]): number {
|
|
207
|
+
const args = [...argv];
|
|
208
|
+
const as_json = args.includes('--json');
|
|
209
|
+
const weekly = args.includes('--weekly');
|
|
210
|
+
const record = args.includes('--record');
|
|
211
|
+
let last = 10;
|
|
212
|
+
for (let i = 0; i < args.length; i++) {
|
|
213
|
+
const a = args[i] as string;
|
|
214
|
+
if (a === '--last') last = Number(args[++i] ?? '10') || 10;
|
|
215
|
+
else if (a.startsWith('--last=')) last = Number(a.slice('--last='.length)) || 10;
|
|
216
|
+
else if (a === '-h' || a === '--help') {
|
|
217
|
+
process.stdout.write('usage: agent-config route:audit [--last N] [--record] [--weekly] [--json]\n');
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (weekly) {
|
|
223
|
+
const { text, records } = render_weekly(recorder_path(), new Date());
|
|
224
|
+
process.stdout.write(text);
|
|
225
|
+
return records === 0 ? 1 : 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let router: Router;
|
|
229
|
+
try {
|
|
230
|
+
router = JSON.parse(fs.readFileSync(ROUTER_JSON, 'utf-8')) as Router;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
process.stderr.write(`route:audit: cannot read ${ROUTER_JSON}: ${String(e)}\n`);
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
const prompts = load_user_prompts(last);
|
|
236
|
+
if (prompts.length === 0) {
|
|
237
|
+
process.stdout.write(MEASUREMENT_HEADER + '\n\n');
|
|
238
|
+
process.stdout.write('no user prompts found in the chat-history log — nothing to audit\n');
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
const audits = audit_prompts(router, prompts);
|
|
242
|
+
|
|
243
|
+
let recorded = 0;
|
|
244
|
+
let recorder_note = '';
|
|
245
|
+
if (record) {
|
|
246
|
+
if (recorder_enabled()) {
|
|
247
|
+
recorded = append_records(build_records(audits, new Date().toISOString()), recorder_path());
|
|
248
|
+
recorder_note = `recorded ${recorded} prompt(s) to ${RECORDER_REL}`;
|
|
249
|
+
} else {
|
|
250
|
+
recorder_note = 'recorder disabled (telemetry.routing_recorder.enabled is off — default) — nothing written';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (as_json) {
|
|
255
|
+
process.stdout.write(
|
|
256
|
+
JSON.stringify(
|
|
257
|
+
{ measurement_level: MEASUREMENT_HEADER, audited: audits.length, recorded, prompts: audits },
|
|
258
|
+
null,
|
|
259
|
+
2,
|
|
260
|
+
) + '\n',
|
|
261
|
+
);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
const out: string[] = [MEASUREMENT_HEADER, ''];
|
|
265
|
+
out.push(`audited the last ${audits.length} user prompt(s) — matched rules per prompt`);
|
|
266
|
+
out.push('(should-have-matched is the reviewer\'s judgment over this table, not a computed verdict)');
|
|
267
|
+
out.push('');
|
|
268
|
+
audits.forEach((a, i) => {
|
|
269
|
+
const shown = a.prompt.length > 100 ? a.prompt.slice(0, 97) + '...' : a.prompt;
|
|
270
|
+
out.push(`${String(i + 1).padStart(3)}. ${JSON.stringify(shown)}`);
|
|
271
|
+
if (a.matched.length === 0) {
|
|
272
|
+
out.push(' → (no trigger matched — kernel only)');
|
|
273
|
+
} else {
|
|
274
|
+
for (const m of a.matched) {
|
|
275
|
+
out.push(` → ${m.rule} [${m.tier}] (${m.triggers.join(' · ')})`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
if (recorder_note) {
|
|
280
|
+
out.push('');
|
|
281
|
+
out.push(recorder_note);
|
|
282
|
+
}
|
|
283
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
288
|
+
if (process.argv[1] && (import.meta.url === pathToFileURL(process.argv[1]).href || path.resolve(process.argv[1]) === _HERE)) {
|
|
289
|
+
process.exitCode = main(process.argv.slice(2));
|
|
290
|
+
}
|