@event4u/agent-config 10.1.0 → 10.3.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 +110 -167
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
- package/dist/agent-src/commands/roadmap/create.md +4 -1
- package/dist/agent-src/commands/roadmap/next.md +38 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
- package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
- package/dist/agent-src/personas/advisors/first-principles.md +6 -0
- package/dist/agent-src/rules/active-remediation.md +53 -2
- package/dist/agent-src/rules/code-provenance.md +66 -4
- package/dist/agent-src/rules/content-quoting-floor.md +6 -0
- package/dist/agent-src/rules/context-hygiene.md +39 -0
- package/dist/agent-src/rules/design-fidelity.md +49 -3
- package/dist/agent-src/rules/token-budget-discipline.md +3 -2
- package/dist/agent-src/rules/token-efficiency.md +22 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +9 -9
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
- package/dist/agent-src/templates/agent-settings.md +12 -28
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
- package/dist/agent-src/templates/persona.md +26 -0
- package/dist/agent-src/templates/roadmaps.md +27 -7
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/dist/agent-src/templates/skill.md +35 -1
- package/dist/cli/commands/commands.candidates.test.js +257 -0
- package/dist/cli/commands/commands.candidates.test.js.map +1 -0
- package/dist/cli/commands/commands.js +199 -0
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
- package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
- package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
- package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
- package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
- package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-FV34EGEX.js} +166 -39
- package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
- package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
- package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
- 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 +881 -204
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- 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 +6 -5
- 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 +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +44 -9
- package/dist/cli-delegate/cmd_settings_get.js +2 -2
- 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 +21 -21
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +204 -44
- package/dist/install/install.mjs +9 -21
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1337 -490
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +25 -2
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/_lib/compile_time_toggles.js +4 -3
- package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
- package/dist/server/io/yamlIO.js +0 -2
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/schemas/settings.js +3 -9
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsClasses.js +64 -2
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
- package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +307 -145
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
- package/docs/guidelines/agent-infra/layered-settings.md +1 -2
- package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
- package/docs/guidelines/design-antipatterns.md +79 -10
- package/docs/guidelines/design-fidelity-mechanics.md +120 -2
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/src/config/agent-settings.template.yml +12 -30
- package/src/config/gate-coverage.yml +42 -8
- package/src/config/gate-violation-baselines.json +15 -3
- package/src/config/profiles/balanced.ini +0 -2
- package/src/config/profiles/full.ini +1 -3
- package/src/config/profiles/minimal.ini +0 -2
- package/src/config/review-prompt-binding-baseline.json +19 -0
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
- package/src/scripts/_cli/handoff_sessions.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -0
- package/src/scripts/_lib/agent_settings.ts +25 -2
- package/src/scripts/_lib/bench_telegraph.ts +1 -1
- package/src/scripts/_lib/cc_transcript.ts +25 -0
- package/src/scripts/_lib/compile_time_toggles.ts +4 -3
- package/src/scripts/ai_council/clients.ts +243 -34
- package/src/scripts/cache_realization_report.ts +3 -2
- package/src/scripts/check_ci_local_parity.ts +60 -6
- package/src/scripts/check_review_prompt_binding.ts +553 -0
- package/src/scripts/check_rule_projection_integrity.ts +3 -1
- package/src/scripts/check_suppression_hygiene.ts +19 -3
- package/src/scripts/conformance_scan.ts +282 -9
- package/src/scripts/cost/track.mjs +10 -4
- package/src/scripts/council_cli.ts +33 -3
- package/src/scripts/design_slop_rules.test.ts +37 -6
- package/src/scripts/design_slop_rules.ts +150 -22
- package/src/scripts/dispatch_r2_reviewer.ts +15 -1
- package/src/scripts/external_sources_denylist.json +3 -1
- package/src/scripts/hook_manifest.yaml +17 -6
- package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
- package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
- package/src/scripts/hooks/evidence_independence.ts +61 -9
- package/src/scripts/hooks/session_eol_hook.ts +123 -3
- package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
- package/src/scripts/lint_design_antipattern_parity.ts +201 -0
- package/src/scripts/lint_harvest_provenance.ts +483 -0
- package/src/scripts/lint_settings_classes.ts +273 -11
- package/src/scripts/lint_skill_router_head.ts +254 -0
- package/src/scripts/lint_symptom_intake.ts +241 -0
- package/src/scripts/report_consultation_rate.ts +128 -2
- package/src/scripts/report_skill_activation.ts +6 -4
- package/src/scripts/report_skill_obligation_violations.ts +2 -1
- package/src/scripts/run_skill_evals.ts +10 -5
- package/src/scripts/schemas/persona.schema.json +8 -0
- package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
- package/src/scripts/sweep_dead_scan_roots.ts +51 -0
- package/src/server/io/yamlIO.ts +0 -2
- package/src/server/schemas/settings.ts +3 -19
- package/src/shared/settingsClasses.ts +93 -2
- package/dist/ui/assets/index-B5qGYOJo.js.map +0 -1
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
* dependency-free approach. Council 2026-06-28 assumed those libs were present;
|
|
18
18
|
* for THIS package, pattern-based is the correct, frugal adaptation.
|
|
19
19
|
*
|
|
20
|
+
* WHAT THE NEGATIVE FIXTURES ARE NOT: each rule ships one positive and one
|
|
21
|
+
* negative fixture, and the suite asserts both exist. That is a PRESENCE guard —
|
|
22
|
+
* "this regex stays quiet on one clean snippet crafted for it" — and it is NOT a
|
|
23
|
+
* false-positive RATE. Nineteen green negatives say nothing about how often a
|
|
24
|
+
* rule fires on clean UI it has never seen. Reading them as an FP measurement is
|
|
25
|
+
* the mistake that makes the registry look better-evidenced than it is; the rate
|
|
26
|
+
* is measured against the clean corpus under `internal/bench/corpora/`, not here.
|
|
27
|
+
*
|
|
20
28
|
* Severity is a REBUTTABLE PRESUMPTION, never a hard block:
|
|
21
29
|
* P0 — objective failure (none here; those live in lint_design_quality)
|
|
22
30
|
* P1 — high-confidence tell (e.g. the side-stripe signature)
|
|
@@ -437,7 +445,7 @@ export const SLOP_RULES: SlopRule[] = [
|
|
|
437
445
|
},
|
|
438
446
|
},
|
|
439
447
|
{
|
|
440
|
-
id: "slop-lock-shape",
|
|
448
|
+
id: "slop-v8-lock-shape",
|
|
441
449
|
catalogId: "V8",
|
|
442
450
|
severity: "P2",
|
|
443
451
|
engines: ["css"],
|
|
@@ -464,27 +472,6 @@ export const SLOP_RULES: SlopRule[] = [
|
|
|
464
472
|
return [];
|
|
465
473
|
},
|
|
466
474
|
},
|
|
467
|
-
{
|
|
468
|
-
id: "slop-lock-colour",
|
|
469
|
-
catalogId: "C6",
|
|
470
|
-
severity: "P3",
|
|
471
|
-
engines: ["css"],
|
|
472
|
-
description: "Colour Lock — multiple saturated accent hue families",
|
|
473
|
-
message: "Several accent hue families read as no single accent identity; lock one (C6 / Colour Lock).",
|
|
474
|
-
gated: (ctx) => ctx.has("colour lock", "color lock"),
|
|
475
|
-
detect: ({ content }) => {
|
|
476
|
-
const buckets = new Set<number>();
|
|
477
|
-
for (const pm of content.matchAll(/#([0-9a-f]{3}|[0-9a-f]{6})\b/gi)) {
|
|
478
|
-
const b = hueBucket("#" + (pm[1] ?? ""));
|
|
479
|
-
if (b !== null) buckets.add(b);
|
|
480
|
-
}
|
|
481
|
-
if (buckets.size >= 3) {
|
|
482
|
-
const idx = content.search(/#[0-9a-f]{3,6}\b/i);
|
|
483
|
-
return [{ line: lineOf(content, Math.max(0, idx)), snippet: `${buckets.size} distinct accent hue families` }];
|
|
484
|
-
}
|
|
485
|
-
return [];
|
|
486
|
-
},
|
|
487
|
-
},
|
|
488
475
|
{
|
|
489
476
|
id: "slop-l9-section-monotony",
|
|
490
477
|
catalogId: "L9",
|
|
@@ -527,4 +514,145 @@ export const SLOP_RULES: SlopRule[] = [
|
|
|
527
514
|
return [];
|
|
528
515
|
},
|
|
529
516
|
},
|
|
517
|
+
// -------------------------------------------------------------------------
|
|
518
|
+
// Catalog thresholds promoted to rules (road-to-design-detector-evidence
|
|
519
|
+
// Phase 3). Each entry below already published its number in
|
|
520
|
+
// docs/guidelines/design-antipatterns.md — the prose is the specification,
|
|
521
|
+
// nothing here is invented, and each was graded per rule against the clean
|
|
522
|
+
// corpus rather than as a batch.
|
|
523
|
+
// -------------------------------------------------------------------------
|
|
524
|
+
{
|
|
525
|
+
id: "slop-v4-over-rounded",
|
|
526
|
+
catalogId: "V4",
|
|
527
|
+
severity: "P3",
|
|
528
|
+
engines: ["css"],
|
|
529
|
+
description: "border-radius > 16px on an element declared under 200px wide",
|
|
530
|
+
message: "Heavy rounding on a small surface reads as scaffold-default; scale the radius to the element (V4).",
|
|
531
|
+
gated: (ctx) => ctx.has("pill", "fully rounded", "soft ui", "claymorphism"),
|
|
532
|
+
detect: ({ content }) =>
|
|
533
|
+
cssBlocks(content)
|
|
534
|
+
.filter((b) => {
|
|
535
|
+
// A pill (9999px / 50% / 999px) and a circle are deliberate shapes,
|
|
536
|
+
// not over-rounding — the tell is a mid-range radius on a small box.
|
|
537
|
+
const r = /border-radius\s*:\s*(\d+)px/i.exec(b.body);
|
|
538
|
+
if (!r?.[1]) return false;
|
|
539
|
+
const radius = parseInt(r[1], 10);
|
|
540
|
+
if (radius <= 16 || radius >= 100) return false;
|
|
541
|
+
// "small" must be declared in the same block; nothing here infers
|
|
542
|
+
// layout, because inferring it would need a cascade.
|
|
543
|
+
const size = /(?:^|[;{\s])(?:max-)?(?:width|height)\s*:\s*(\d+)px/i.exec(b.body);
|
|
544
|
+
return size?.[1] !== undefined && parseInt(size[1], 10) < 200;
|
|
545
|
+
})
|
|
546
|
+
.map((b) => ({ line: b.line, snippet: b.selector.slice(0, 80) })),
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
id: "slop-c3-dark-glow",
|
|
550
|
+
catalogId: "C3",
|
|
551
|
+
severity: "P2",
|
|
552
|
+
engines: ["css"],
|
|
553
|
+
description: "Saturated zero-offset glow (box-shadow / text-shadow) in a dark context",
|
|
554
|
+
message: "A neon glow on dark reads as gaming-UI default rather than a considered accent (C3).",
|
|
555
|
+
gated: (ctx) => ctx.has("glow", "neon", "cyberpunk", "gaming"),
|
|
556
|
+
detect: ({ content, lines }) => {
|
|
557
|
+
// The tell is dark-context-specific, so a file with no dark surface at all
|
|
558
|
+
// is out of scope — a glow on a light page is a different question.
|
|
559
|
+
const darkContext =
|
|
560
|
+
/prefers-color-scheme\s*:\s*dark/i.test(content) ||
|
|
561
|
+
/\.dark\b|\[data-theme=["']?dark/i.test(content) ||
|
|
562
|
+
/background(?:-color)?\s*:\s*#(?:0[0-9a-f]{5}|1[0-9a-f]{5}|000|111)\b/i.test(content);
|
|
563
|
+
if (!darkContext) return [];
|
|
564
|
+
const out: RawHit[] = [];
|
|
565
|
+
for (let i = 0; i < lines.length; i++) {
|
|
566
|
+
const l = lines[i] ?? "";
|
|
567
|
+
// zero-offset + real blur is the glow signature; an offset shadow is depth.
|
|
568
|
+
const m = /(?:box|text)-shadow\s*:\s*0\s+0\s+(\d+)px[^;]*?(#[0-9a-f]{3,8})/i.exec(l);
|
|
569
|
+
if (!m?.[1] || !m[2]) continue;
|
|
570
|
+
if (parseInt(m[1], 10) < 8) continue;
|
|
571
|
+
if (hueBucket(m[2]) === null) continue; // a neutral halo is not a neon accent
|
|
572
|
+
out.push({ line: i + 1, snippet: l.trim().slice(0, 80) });
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: "slop-t9-uppercase-body",
|
|
579
|
+
catalogId: "T9",
|
|
580
|
+
severity: "P3",
|
|
581
|
+
engines: ["css"],
|
|
582
|
+
description: "text-transform: uppercase applied to body-length text",
|
|
583
|
+
message: "All-caps destroys word-shape for sustained reading; reserve it for short labels (T9).",
|
|
584
|
+
gated: (ctx) => ctx.has("all caps", "all-caps", "uppercase display"),
|
|
585
|
+
detect: ({ content }) =>
|
|
586
|
+
cssBlocks(content)
|
|
587
|
+
// Short UI surfaces (labels, buttons, table headers, badges, eyebrows)
|
|
588
|
+
// are the legitimate home of uppercase and are excluded by selector.
|
|
589
|
+
.filter((b) => /\b(p|body|article|\.prose|\.body|\.content|\.copy|blockquote|li)\b/i.test(b.selector))
|
|
590
|
+
.filter((b) => !/label|button|\bth\b|badge|chip|tag|eyebrow|kicker|caption|nav/i.test(b.selector))
|
|
591
|
+
.filter((b) => /text-transform\s*:\s*uppercase/i.test(b.body))
|
|
592
|
+
.map((b) => ({ line: b.line, snippet: b.selector.slice(0, 80) })),
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
id: "slop-t10-wide-tracking-body",
|
|
596
|
+
catalogId: "T10",
|
|
597
|
+
severity: "P3",
|
|
598
|
+
engines: ["css"],
|
|
599
|
+
description: "letter-spacing above 0.05em on body-length text",
|
|
600
|
+
message: "Wide tracking slows sustained reading; it belongs to display type, not body (T10).",
|
|
601
|
+
gated: (ctx) => ctx.has("wide tracking", "letterspacing", "letter-spacing"),
|
|
602
|
+
detect: ({ content }) =>
|
|
603
|
+
cssBlocks(content)
|
|
604
|
+
.filter((b) => /\b(p|body|article|\.prose|\.body|\.content|\.copy|blockquote|li)\b/i.test(b.selector))
|
|
605
|
+
.filter((b) => !/label|button|\bth\b|badge|chip|tag|eyebrow|kicker|caption|nav|h[1-6]/i.test(b.selector))
|
|
606
|
+
.filter((b) => {
|
|
607
|
+
const em = /letter-spacing\s*:\s*(\d*\.?\d+)(em|rem)/i.exec(b.body);
|
|
608
|
+
if (em?.[1]) return parseFloat(em[1]) > 0.05;
|
|
609
|
+
const px = /letter-spacing\s*:\s*(\d*\.?\d+)px/i.exec(b.body);
|
|
610
|
+
return px?.[1] !== undefined && parseFloat(px[1]) > 0.8;
|
|
611
|
+
})
|
|
612
|
+
.map((b) => ({ line: b.line, snippet: b.selector.slice(0, 80) })),
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: "slop-m1-bounce-easing",
|
|
616
|
+
catalogId: "M1",
|
|
617
|
+
severity: "P2",
|
|
618
|
+
engines: ["css"],
|
|
619
|
+
description: "Bounce or elastic easing on a UI transition",
|
|
620
|
+
message: "Overshoot draws attention to the animation instead of the state change (M1).",
|
|
621
|
+
gated: (ctx) => ctx.has("bounce", "playful motion", "elastic"),
|
|
622
|
+
detect: ({ lines }) => {
|
|
623
|
+
const out: RawHit[] = [];
|
|
624
|
+
for (let i = 0; i < lines.length; i++) {
|
|
625
|
+
const l = lines[i] ?? "";
|
|
626
|
+
// Overshoot is decidable from the curve itself: a control point outside
|
|
627
|
+
// [0,1] on the output axis means the value passes its target and returns.
|
|
628
|
+
const cb = /cubic-bezier\(\s*[-\d.]+\s*,\s*(-?[\d.]+)\s*,\s*[-\d.]+\s*,\s*(-?[\d.]+)\s*\)/i.exec(l);
|
|
629
|
+
if (cb?.[1] !== undefined && cb[2] !== undefined) {
|
|
630
|
+
const y1 = parseFloat(cb[1]);
|
|
631
|
+
const y2 = parseFloat(cb[2]);
|
|
632
|
+
if (y1 < 0 || y1 > 1 || y2 < 0 || y2 > 1) {
|
|
633
|
+
out.push({ line: i + 1, snippet: l.trim().slice(0, 80) });
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (/(?:transition|animation)[^;]*\b(?:bounce|elastic|back(?:In|Out|InOut))\b/i.test(l)) {
|
|
638
|
+
out.push({ line: i + 1, snippet: l.trim().slice(0, 80) });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "slop-m3-img-hover-transform",
|
|
646
|
+
catalogId: "M3",
|
|
647
|
+
severity: "P3",
|
|
648
|
+
engines: ["css"],
|
|
649
|
+
description: "transform or filter animation on an <img> hover",
|
|
650
|
+
message: "Hover-scaling an image triggers composite plus decode and often shifts layout (M3).",
|
|
651
|
+
gated: (ctx) => ctx.has("image hover", "hover zoom", "ken burns"),
|
|
652
|
+
detect: ({ content }) =>
|
|
653
|
+
cssBlocks(content)
|
|
654
|
+
.filter((b) => /\bimg\b/i.test(b.selector) && /:hover|:focus-within/i.test(b.selector))
|
|
655
|
+
.filter((b) => /(?:^|[;{\s])(?:transform|filter)\s*:/i.test(b.body))
|
|
656
|
+
.map((b) => ({ line: b.line, snippet: b.selector.slice(0, 80) })),
|
|
657
|
+
},
|
|
530
658
|
];
|
|
@@ -642,7 +642,21 @@ function reviewerPrompt(args: {
|
|
|
642
642
|
* Writing the hash of the prompt THIS dispatcher built makes the channel
|
|
643
643
|
* attributable: the artefact now records which prompt produced it, so a review
|
|
644
644
|
* run against a different prompt is distinguishable from one run against the
|
|
645
|
-
* dispatcher's.
|
|
645
|
+
* dispatcher's.
|
|
646
|
+
*
|
|
647
|
+
* `--verify` / `--verify-current` do NOT re-derive it. {@link runVerify}
|
|
648
|
+
* re-derives `scope_hash`, `roadmap_hash` and `ac_hash` only — it re-computes
|
|
649
|
+
* the review's INPUTS from the current repo state, and the prompt is not one of
|
|
650
|
+
* them (it is not reconstructible from the repo; it is an artefact this
|
|
651
|
+
* dispatcher wrote). An earlier revision of this comment claimed otherwise for
|
|
652
|
+
* roughly a year, which is how the field came to be written, parsed, and
|
|
653
|
+
* compared to nothing.
|
|
654
|
+
*
|
|
655
|
+
* What compares it is `check_review_prompt_binding`, against the prompt package
|
|
656
|
+
* this dispatcher commits beside the artefact (`<slug>.review-input/prompt.md`,
|
|
657
|
+
* written below) — a different check with a different scope: corpus-wide rather
|
|
658
|
+
* than current-branch, because a committed prompt and its recorded hash are both
|
|
659
|
+
* immutable.
|
|
646
660
|
*
|
|
647
661
|
* It does not CLOSE the residual and is not sold as doing so: the same host that
|
|
648
662
|
* authors a steered prompt could hash the steered text. The gain is an
|
|
@@ -306,12 +306,23 @@ concerns:
|
|
|
306
306
|
args: []
|
|
307
307
|
fail_closed: true
|
|
308
308
|
severity: blocking
|
|
309
|
-
# Evaluator independence. Gates EVALUATION dispatches only
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
#
|
|
309
|
+
# Evaluator independence. Gates EVALUATION dispatches only. Two branches with
|
|
310
|
+
# DIFFERENT severities, per the tier rule in
|
|
311
|
+
# docs/contracts/hook-architecture-v1.md (§ What a concern may block on):
|
|
312
|
+
# · pre-loaded verdict → BLOCKS. It matches a literal steering formulation
|
|
313
|
+
# ("NO-FINDINGS is expected and welcome"), which IS the violation rather
|
|
314
|
+
# than evidence of one.
|
|
315
|
+
# · second self-scoped dispatch in a turn → WARNS (exit 0 + decision:"warn").
|
|
316
|
+
# It decides from prose alone — Tier 3 — and a cross-project audit measured
|
|
317
|
+
# the cost of blocking on that: a 16-way IMPLEMENTATION fan-out lost 15 of
|
|
318
|
+
# its workers, because the phrase `this branch` (an address every worktree
|
|
319
|
+
# dispatch prompt carries) read as a self-reference.
|
|
320
|
+
# `severity: blocking` therefore states the HIGHEST severity this concern can
|
|
321
|
+
# reach, not the severity of every path through it.
|
|
322
|
+
# Ordinary parallel fan-out was always meant to be untouched: the council's
|
|
323
|
+
# "second dispatch of any kind" criterion would have blocked 6 of the 7 analysis
|
|
324
|
+
# subagents in the very session that produced this guard. fail_closed: false — a
|
|
325
|
+
# malformed envelope must never block a dispatch.
|
|
315
326
|
evidence-independence:
|
|
316
327
|
script: src/scripts/hooks/evidence_independence.ts
|
|
317
328
|
args: []
|
|
@@ -105,25 +105,71 @@ export const WARN_OPS: ReadonlySet<GitOp> = new Set<GitOp>([
|
|
|
105
105
|
// gate. `G` allows any run of them.
|
|
106
106
|
const G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
|
|
107
107
|
|
|
108
|
+
/** Optional absolute/relative path in front of the tool word: `/usr/bin/npm`. */
|
|
109
|
+
const P = "^(?:\\S*\\/)?";
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A `gh api` call that WRITES. Read-only `gh api` is the standard way to inspect
|
|
113
|
+
* GitHub state and must never be refused.
|
|
114
|
+
*
|
|
115
|
+
* Two lookaheads rather than one linear pattern, because `-X POST` may appear on
|
|
116
|
+
* either side of the path: `gh api -X POST repos/o/r/releases` and
|
|
117
|
+
* `gh api repos/o/r/releases -X POST` are the same call.
|
|
118
|
+
*/
|
|
119
|
+
function ghApiWrite(pathRe: string): RegExp {
|
|
120
|
+
return new RegExp(
|
|
121
|
+
`${P}gh\\s+api\\b(?=[^\\n]*${pathRe})(?=[^\\n]*(?:-X|--method)\\s+(?:POST|PATCH|PUT|DELETE)\\b)`,
|
|
122
|
+
"i",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
108
126
|
/**
|
|
109
127
|
* Command → operation. Order matters: the most specific pattern wins, so
|
|
110
128
|
* `git push --tags` is a tag push and not a plain push.
|
|
129
|
+
*
|
|
130
|
+
* Every pattern is anchored at command position (`P`), not `\b`. An unanchored
|
|
131
|
+
* verb matches its own name inside a QUOTED ARGUMENT, and the 2026-08-12 session
|
|
132
|
+
* audit measured all three of that window's blocked ops as exactly that false
|
|
133
|
+
* positive — every one read-only or harmless:
|
|
134
|
+
*
|
|
135
|
+
* gh pr create … --title "…(unblock npm publish)" → classified `publish`
|
|
136
|
+
* gh api repos/o/r/releases/latest --jq … → classified `release`
|
|
137
|
+
* gh api repos/jdx/aube-action/releases --jq … → classified `release`
|
|
138
|
+
*
|
|
139
|
+
* The first is a PR *about* a publishing problem; the other two are GETs, one of
|
|
140
|
+
* them against a third-party repo. All three are BLOCK ops, so on a hook-bound
|
|
141
|
+
* host the operator could neither open that PR nor diagnose why a release had
|
|
142
|
+
* not appeared — while the transcripts of that same window carry "Ich kann
|
|
143
|
+
* wieder nicht releasen. Warum nicht?".
|
|
144
|
+
*
|
|
145
|
+
* The heredoc stripper upstream already removes `--body "$(cat <<EOF …)"`
|
|
146
|
+
* payloads; it cannot help here because `--title` is an ordinary quoted
|
|
147
|
+
* argument. Anchoring is what separates *invoking* a verb from *naming* one.
|
|
111
148
|
*/
|
|
112
149
|
const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
113
|
-
{ op: "publish", re: new RegExp(
|
|
114
|
-
{
|
|
150
|
+
{ op: "publish", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}publish\\b`, "i") },
|
|
151
|
+
{
|
|
152
|
+
op: "release",
|
|
153
|
+
re: new RegExp(`${P}gh\\s+release\\s+create\\b|${ghApiWrite("\\/releases\\b").source}`, "i"),
|
|
154
|
+
},
|
|
115
155
|
{
|
|
116
156
|
op: "tag",
|
|
157
|
+
re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--tags|--follow-tags|refs\\/tags\\/)`, "i"),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
op: "pr-merge",
|
|
117
161
|
re: new RegExp(
|
|
118
|
-
|
|
162
|
+
`${P}gh\\s+pr\\s+merge\\b|${ghApiWrite("\\/pulls\\/\\d+\\/merge\\b").source}`,
|
|
119
163
|
"i",
|
|
120
164
|
),
|
|
121
165
|
},
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{ op: "
|
|
166
|
+
{
|
|
167
|
+
op: "pr-create",
|
|
168
|
+
re: new RegExp(`${P}gh\\s+pr\\s+create\\b|${ghApiWrite("\\/pulls\\b").source}`, "i"),
|
|
169
|
+
},
|
|
170
|
+
{ op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
|
|
171
|
+
{ op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
|
|
172
|
+
{ op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") },
|
|
127
173
|
];
|
|
128
174
|
|
|
129
175
|
const COMMAND_TOOLS: ReadonlySet<string> = new Set([
|
|
@@ -128,10 +128,35 @@ function _isObject(v: unknown): v is JsonObject {
|
|
|
128
128
|
const FILE_TOKEN_RE =
|
|
129
129
|
/`?\b[\w][\w.\-]{0,80}\.(ts|tsx|js|jsx|mjs|cjs|py|php|rb|go|rs|java|kt|swift|md|mdx|json|ya?ml|css|scss|less|html|vue|svelte|sql|sh|bash|c|cpp|h|hpp|cs|toml|xml)\b`?/gi;
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
/**
|
|
132
|
+
* The unique file tokens themselves, lower-cased and stripped of backticks.
|
|
133
|
+
*
|
|
134
|
+
* `detectEnumeratedFiles` needs only the size, but `conformance_scan`'s
|
|
135
|
+
* `task-completeness` check needs the IDENTITIES: it asks which of the files a
|
|
136
|
+
* prompt enumerated were actually touched, which a count cannot answer. Exposing
|
|
137
|
+
* them here rather than re-matching `FILE_TOKEN_RE` in the scanner keeps one
|
|
138
|
+
* regex in the tree — a second copy is the "second artefact to keep in sync"
|
|
139
|
+
* that would let the classifier and the measurement disagree silently, which is
|
|
140
|
+
* the property `conformance_scan`'s own header requires of every classifier it
|
|
141
|
+
* imports.
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* Minimum unique file tokens before the FILE shape counts as a delegable
|
|
145
|
+
* signal (F7, review). Named rather than inlined because `conformance_scan`'s
|
|
146
|
+
* `task-completeness` check gates on the same floor: a prompt whose file
|
|
147
|
+
* enumeration is below it is not treated as naming a deliverable SET by either
|
|
148
|
+
* reader, and two copies of the number would let them disagree about which
|
|
149
|
+
* prompts are in scope.
|
|
150
|
+
*/
|
|
151
|
+
export const FILE_SIGNAL_FLOOR = 3;
|
|
152
|
+
|
|
153
|
+
export function enumeratedFileTokens(text: string): string[] {
|
|
132
154
|
const matches = text.match(FILE_TOKEN_RE) ?? [];
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
return [...new Set(matches.map((m) => m.replace(/`/g, "").toLowerCase()))];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function detectEnumeratedFiles(text: string): number {
|
|
159
|
+
return enumeratedFileTokens(text).length;
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
/**
|
|
@@ -246,12 +271,12 @@ export function extractTaskSignals(text: string): ExtractedSignals {
|
|
|
246
271
|
const conjunction = detectMultiDeliverableConjunction(text);
|
|
247
272
|
const forEach = detectForEachShape(text);
|
|
248
273
|
|
|
249
|
-
// F7 (review): a bare file-token count below
|
|
250
|
-
// evidence of independent, parallelizable work — "rename a.ts to
|
|
251
|
-
// names 2 files but is one mechanical edit, not two delegable slices.
|
|
252
|
-
// explicit "N files/Dateien" phrase (`detectExplicitSliceCount`) is a
|
|
274
|
+
// F7 (review): a bare file-token count below FILE_SIGNAL_FLOOR is common and
|
|
275
|
+
// NOT itself evidence of independent, parallelizable work — "rename a.ts to
|
|
276
|
+
// b.ts" names 2 files but is one mechanical edit, not two delegable slices.
|
|
277
|
+
// The explicit "N files/Dateien" phrase (`detectExplicitSliceCount`) is a
|
|
253
278
|
// stronger, author-stated signal and keeps its existing floor of N>=2.
|
|
254
|
-
const fileSignal = fileCount >=
|
|
279
|
+
const fileSignal = fileCount >= FILE_SIGNAL_FLOOR ? fileCount : 0;
|
|
255
280
|
|
|
256
281
|
let slices = Math.max(fileSignal, explicit, conjunction);
|
|
257
282
|
if (slices === 0 && forEach) {
|
|
@@ -135,9 +135,27 @@ export function isEvaluationPrompt(prompt: string): boolean {
|
|
|
135
135
|
* ON, so the second-dispatch block requires a self-reference. The pre-loaded-
|
|
136
136
|
* verdict block does NOT: steering an evaluator is wrong whatever it is
|
|
137
137
|
* pointed at.
|
|
138
|
+
*
|
|
139
|
+
* That fix was still too wide, and the cross-project session audit (2026-08-12)
|
|
140
|
+
* measured the cost: `road-to-release-truth/fc1ff181` turn 3 fanned out 16
|
|
141
|
+
* IMPLEMENTATION workers, and 15 were classified self-scoped on one phrase —
|
|
142
|
+
* `this branch`. In context it read "two conversions already landed on this
|
|
143
|
+
* branch in exactly the style you should match", i.e. it named WHERE the work
|
|
144
|
+
* happens. A worktree dispatch prompt carries that phrase by construction
|
|
145
|
+
* ("Work in <worktree> (branch …)"), so on a hook-bound host the guard ate 15
|
|
146
|
+
* of 16 workers — the exact behaviour `evaluator-independence` § "When it does
|
|
147
|
+
* NOT fire" promises is out of scope ("…or IMPLEMENT is not evaluation and is
|
|
148
|
+
* not gated").
|
|
149
|
+
*
|
|
150
|
+
* So the self-reference must name a SUBJECT a verdict can be shopped for — a
|
|
151
|
+
* diff, a change, a patch — never the LOCATION the work happens in. `this
|
|
152
|
+
* branch` is dropped for that reason; `my branch` stays, because the possessive
|
|
153
|
+
* makes it a claim about the agent's own work rather than an address. `this pr`
|
|
154
|
+
* stays for the same reason `this diff` does: a PR is a thing reviewers render
|
|
155
|
+
* verdicts on, whereas a branch is where you stand while working.
|
|
138
156
|
*/
|
|
139
157
|
const SELF_SCOPE_RE =
|
|
140
|
-
/\b(my (work|change|diff|implementation|code|fix|patch|branch|pr)|this (diff|change|
|
|
158
|
+
/\b(my (work|change|diff|implementation|code|fix|patch|branch|pr)|this (diff|change|pr|delta|implementation|patch)|the delta|the change i|what i (wrote|built|changed|implemented)|i just (wrote|built|changed|implemented))\b/i;
|
|
141
159
|
|
|
142
160
|
export function isSelfScoped(prompt: string): boolean {
|
|
143
161
|
return SELF_SCOPE_RE.test(prompt);
|
|
@@ -251,16 +269,50 @@ export function decide(
|
|
|
251
269
|
return { exit: EXIT_ALLOW, stdout: "", stderr: "", evaluations: priorEvaluations };
|
|
252
270
|
}
|
|
253
271
|
|
|
272
|
+
// WARN, not block — a deliberate severity decision, not an oversight.
|
|
273
|
+
//
|
|
274
|
+
// This branch decides from PROSE ALONE: `isEvaluationPrompt` and `isSelfScoped`
|
|
275
|
+
// both infer intent from a natural-language prompt, and no structured fact
|
|
276
|
+
// corroborates them. Under the tier rule in `docs/contracts/hook-architecture-v1.md`
|
|
277
|
+
// (§ What a concern may block on) that is Tier 3, and Tier 3 may only warn.
|
|
278
|
+
//
|
|
279
|
+
// It is written from measurement, not taste. A 16-way fan-out of IMPLEMENTATION
|
|
280
|
+
// subagents lost 15 workers to this branch, because `isEvaluationPrompt` fired
|
|
281
|
+
// on the unavoidable words review/audit/check and `isSelfScoped` on the phrase
|
|
282
|
+
// `this branch` — an address every worktree dispatch prompt carries. Narrowing
|
|
283
|
+
// the phrase list fixed that instance; the council convened on the design
|
|
284
|
+
// (anthropic + openai, 2026-08-12, quorum 2/2) held that no finite pattern can
|
|
285
|
+
// bound the false-positive set, and that a prompt — unlike a shell command —
|
|
286
|
+
// has no grammar to anchor an "invoked vs named" discriminator to.
|
|
287
|
+
//
|
|
288
|
+
// What is NOT downgraded: the pre-loaded-verdict block above. That one matches
|
|
289
|
+
// a literal steering formulation ("NO-FINDINGS is expected and welcome"), which
|
|
290
|
+
// IS the violation rather than evidence of one, so it keeps blocking.
|
|
291
|
+
//
|
|
292
|
+
// The route back to blocking is structural, not another regex: a `role` /
|
|
293
|
+
// `evidence_scope` field the dispatcher sets at the call site, where the caller
|
|
294
|
+
// knows by construction whether it is commissioning an evaluation. Then this
|
|
295
|
+
// branch reads a field instead of guessing, and becomes Tier 1.
|
|
296
|
+
// exit 0 + `decision: "warn"` on stdout is how this dispatcher carries an
|
|
297
|
+
// advisory — NOT exit 2. The internal ladder's `2 = warn` is read as BLOCK by
|
|
298
|
+
// Claude Code's native PreToolUse contract, which is the defect that made an
|
|
299
|
+
// advisory guard a hard deny once already; the same shape here would have kept
|
|
300
|
+
// the fan-out blocked while claiming to warn.
|
|
254
301
|
if (priorEvaluations >= 1) {
|
|
255
302
|
return {
|
|
256
|
-
exit:
|
|
257
|
-
stdout:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
303
|
+
exit: EXIT_ALLOW,
|
|
304
|
+
stdout: `${JSON.stringify({
|
|
305
|
+
decision: "warn",
|
|
306
|
+
reason:
|
|
307
|
+
`Second evaluation dispatch of your own work in this turn. If both passes ` +
|
|
308
|
+
`judge the SAME subject, that is verdict shopping — commissioning another ` +
|
|
309
|
+
`with a different prompt or scope selects the answer instead of measuring ` +
|
|
310
|
+
`it. Report what the first pass returned, then re-plan. If this is an ` +
|
|
311
|
+
`implementation fan-out rather than a second review, carry on: this check ` +
|
|
312
|
+
`reads prose and cannot tell the two apart with certainty.`,
|
|
313
|
+
})}\n`,
|
|
314
|
+
stderr: "",
|
|
315
|
+
evaluations: priorEvaluations + 1,
|
|
264
316
|
};
|
|
265
317
|
}
|
|
266
318
|
|