@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.
Files changed (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +110 -167
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
  5. package/dist/agent-src/commands/roadmap/create.md +4 -1
  6. package/dist/agent-src/commands/roadmap/next.md +38 -0
  7. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
  8. package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
  9. package/dist/agent-src/personas/advisors/first-principles.md +6 -0
  10. package/dist/agent-src/rules/active-remediation.md +53 -2
  11. package/dist/agent-src/rules/code-provenance.md +66 -4
  12. package/dist/agent-src/rules/content-quoting-floor.md +6 -0
  13. package/dist/agent-src/rules/context-hygiene.md +39 -0
  14. package/dist/agent-src/rules/design-fidelity.md +49 -3
  15. package/dist/agent-src/rules/token-budget-discipline.md +3 -2
  16. package/dist/agent-src/rules/token-efficiency.md +22 -0
  17. package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
  18. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
  20. package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
  21. package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
  22. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
  23. package/dist/agent-src/skills/roadmap-writing/SKILL.md +9 -9
  24. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
  25. package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
  26. package/dist/agent-src/templates/agent-settings.md +12 -28
  27. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  28. package/dist/agent-src/templates/persona.md +26 -0
  29. package/dist/agent-src/templates/roadmaps.md +27 -7
  30. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  31. package/dist/agent-src/templates/skill.md +35 -1
  32. package/dist/cli/commands/commands.candidates.test.js +257 -0
  33. package/dist/cli/commands/commands.candidates.test.js.map +1 -0
  34. package/dist/cli/commands/commands.js +199 -0
  35. package/dist/cli/commands/commands.js.map +1 -1
  36. package/dist/cli/main.js +1 -0
  37. package/dist/cli/main.js.map +1 -1
  38. package/dist/cli/registry.js +2 -2
  39. package/dist/cli/registry.js.map +1 -1
  40. package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
  41. package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
  42. package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
  43. package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
  44. package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
  45. package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-FV34EGEX.js} +166 -39
  46. package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
  47. package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
  48. package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
  49. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  50. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  51. package/dist/cli-delegate/cmd_conformance.js +881 -204
  52. package/dist/cli-delegate/cmd_doctor.js +3 -3
  53. package/dist/cli-delegate/cmd_explain.js +2 -2
  54. package/dist/cli-delegate/cmd_export.js +4 -4
  55. package/dist/cli-delegate/cmd_handoff.js +6 -5
  56. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  57. package/dist/cli-delegate/cmd_migrate.js +2 -2
  58. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  59. package/dist/cli-delegate/cmd_prune.js +4 -4
  60. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  61. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  62. package/dist/cli-delegate/cmd_session_recycle.js +44 -9
  63. package/dist/cli-delegate/cmd_settings_get.js +2 -2
  64. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  65. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  66. package/dist/cli-delegate/cmd_sync.js +4 -4
  67. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  68. package/dist/cli-delegate/cmd_update.js +1 -1
  69. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  70. package/dist/cli-delegate/cmd_validate.js +4 -4
  71. package/dist/cli-delegate/cmd_versions.js +1 -1
  72. package/dist/discovery/deprecation-report.md +1 -1
  73. package/dist/discovery/discovery-manifest.json +21 -21
  74. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  75. package/dist/discovery/discovery-manifest.summary.md +1 -1
  76. package/dist/discovery/dormancy-report.md +1 -1
  77. package/dist/discovery/orphan-report.md +1 -1
  78. package/dist/discovery/packs.json +2 -2
  79. package/dist/discovery/trust-report.md +1 -1
  80. package/dist/discovery/workspaces.json +2 -2
  81. package/dist/hooks/dispatch.js +204 -44
  82. package/dist/install/install.mjs +9 -21
  83. package/dist/mcp/registry-manifest.json +2 -2
  84. package/dist/mcp/server.json +2 -2
  85. package/dist/mcp/server.mjs +1337 -490
  86. package/dist/router.json +1 -1
  87. package/dist/scripts/_lib/agent_settings.js +25 -2
  88. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  89. package/dist/scripts/_lib/compile_time_toggles.js +4 -3
  90. package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
  91. package/dist/server/io/yamlIO.js +0 -2
  92. package/dist/server/io/yamlIO.js.map +1 -1
  93. package/dist/server/schemas/settings.js +3 -9
  94. package/dist/server/schemas/settings.js.map +1 -1
  95. package/dist/shared/settingsClasses.js +64 -2
  96. package/dist/shared/settingsClasses.js.map +1 -1
  97. package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
  98. package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
  99. package/dist/ui/index.html +1 -1
  100. package/docs/contracts/settings-classes.md +307 -145
  101. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
  102. package/docs/guidelines/agent-infra/layered-settings.md +1 -2
  103. package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
  104. package/docs/guidelines/design-antipatterns.md +79 -10
  105. package/docs/guidelines/design-fidelity-mechanics.md +120 -2
  106. package/package.json +1 -1
  107. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  108. package/src/config/agent-settings.template.yml +12 -30
  109. package/src/config/gate-coverage.yml +42 -8
  110. package/src/config/gate-violation-baselines.json +15 -3
  111. package/src/config/profiles/balanced.ini +0 -2
  112. package/src/config/profiles/full.ini +1 -3
  113. package/src/config/profiles/minimal.ini +0 -2
  114. package/src/config/review-prompt-binding-baseline.json +19 -0
  115. package/src/config/rule-enforcement-baseline.json +0 -1
  116. package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
  117. package/src/scripts/_cli/handoff_sessions.ts +3 -2
  118. package/src/scripts/_dispatch.bash +1 -0
  119. package/src/scripts/_lib/agent_settings.ts +25 -2
  120. package/src/scripts/_lib/bench_telegraph.ts +1 -1
  121. package/src/scripts/_lib/cc_transcript.ts +25 -0
  122. package/src/scripts/_lib/compile_time_toggles.ts +4 -3
  123. package/src/scripts/ai_council/clients.ts +243 -34
  124. package/src/scripts/cache_realization_report.ts +3 -2
  125. package/src/scripts/check_ci_local_parity.ts +60 -6
  126. package/src/scripts/check_review_prompt_binding.ts +553 -0
  127. package/src/scripts/check_rule_projection_integrity.ts +3 -1
  128. package/src/scripts/check_suppression_hygiene.ts +19 -3
  129. package/src/scripts/conformance_scan.ts +282 -9
  130. package/src/scripts/cost/track.mjs +10 -4
  131. package/src/scripts/council_cli.ts +33 -3
  132. package/src/scripts/design_slop_rules.test.ts +37 -6
  133. package/src/scripts/design_slop_rules.ts +150 -22
  134. package/src/scripts/dispatch_r2_reviewer.ts +15 -1
  135. package/src/scripts/external_sources_denylist.json +3 -1
  136. package/src/scripts/hook_manifest.yaml +17 -6
  137. package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
  138. package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
  139. package/src/scripts/hooks/evidence_independence.ts +61 -9
  140. package/src/scripts/hooks/session_eol_hook.ts +123 -3
  141. package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
  142. package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
  143. package/src/scripts/lint_design_antipattern_parity.ts +201 -0
  144. package/src/scripts/lint_harvest_provenance.ts +483 -0
  145. package/src/scripts/lint_settings_classes.ts +273 -11
  146. package/src/scripts/lint_skill_router_head.ts +254 -0
  147. package/src/scripts/lint_symptom_intake.ts +241 -0
  148. package/src/scripts/report_consultation_rate.ts +128 -2
  149. package/src/scripts/report_skill_activation.ts +6 -4
  150. package/src/scripts/report_skill_obligation_violations.ts +2 -1
  151. package/src/scripts/run_skill_evals.ts +10 -5
  152. package/src/scripts/schemas/persona.schema.json +8 -0
  153. package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
  154. package/src/scripts/sweep_dead_scan_roots.ts +51 -0
  155. package/src/server/io/yamlIO.ts +0 -2
  156. package/src/server/schemas/settings.ts +3 -19
  157. package/src/shared/settingsClasses.ts +93 -2
  158. 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. `--verify` re-derives it from the same inputs and compares.
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
@@ -59,7 +59,9 @@
59
59
  "graphify-labs",
60
60
  "penpax",
61
61
  "\\bagent-reach\\b",
62
- "panniantong"
62
+ "panniantong",
63
+ "virgiliojr94",
64
+ "book-to-skill"
63
65
  ],
64
66
  "skip_paths": [
65
67
  "src/scripts/check_no_external_sources.py",
@@ -306,12 +306,23 @@ concerns:
306
306
  args: []
307
307
  fail_closed: true
308
308
  severity: blocking
309
- # Evaluator independence. Gates EVALUATION dispatches only — a verdict
310
- # pre-loaded into the prompt, or a second evaluation of the same work in one
311
- # turn. Ordinary parallel fan-out is untouched: the council's "second dispatch
312
- # of any kind" criterion would have blocked 6 of the 7 analysis subagents in
313
- # the very session that produced this guard. fail_closed: false — a malformed
314
- # envelope must never block a dispatch.
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(`\\b(npm|pnpm|yarn)\\s+${G}publish\\b`, "i") },
114
- { op: "release", re: /\bgh\s+release\s+create\b|\bgh\s+api\b[^\n]*\/releases\b/i },
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
- `\\bgit\\s+${G}push\\b[^\\n;|&]*(--tags|--follow-tags|refs\\/tags\\/)`,
162
+ `${P}gh\\s+pr\\s+merge\\b|${ghApiWrite("\\/pulls\\/\\d+\\/merge\\b").source}`,
119
163
  "i",
120
164
  ),
121
165
  },
122
- { op: "pr-merge", re: /\bgh\s+pr\s+merge\b|\bgh\s+api\b[^\n]*\/pulls\/\d+\/merge\b/i },
123
- { op: "pr-create", re: /\bgh\s+pr\s+create\b|\bgh\s+api\b[^\n]*-X\s+POST[^\n]*\/pulls\b/i },
124
- { op: "push", re: new RegExp(`\\bgit\\s+${G}push\\b`, "i") },
125
- { op: "commit", re: new RegExp(`\\bgit\\s+${G}commit\\b`, "i") },
126
- { op: "branch", re: new RegExp(`\\bgit\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") },
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
- export function detectEnumeratedFiles(text: string): number {
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
- const unique = new Set(matches.map((m) => m.replace(/`/g, "").toLowerCase()));
134
- return unique.size;
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 3 is common and NOT itself
250
- // evidence of independent, parallelizable work — "rename a.ts to b.ts"
251
- // names 2 files but is one mechanical edit, not two delegable slices. The
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 >= 3 ? fileCount : 0;
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|branch|pr|delta|implementation|patch)|the delta|the change i|what i (wrote|built|changed|implemented)|i just (wrote|built|changed|implemented))\b/i;
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: EXIT_BLOCK,
257
- stdout: "",
258
- stderr:
259
- `Blocked: second evaluation dispatch of your own work in this turn (verdict ` +
260
- `shopping). One evaluation has already run; commissioning another with a ` +
261
- `different prompt or scope selects the answer instead of measuring it. ` +
262
- `Report what the first pass returned, then re-plan.\n`,
263
- evaluations: priorEvaluations,
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