@event4u/agent-config 7.0.2 → 7.2.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 (140) hide show
  1. package/.claude-plugin/marketplace.json +9 -1
  2. package/CHANGELOG.md +117 -0
  3. package/README.md +4 -2
  4. package/dist/agent-src/commands/council/default.md +28 -20
  5. package/dist/agent-src/commands/evals/fix.json +5 -1
  6. package/dist/agent-src/commands/fix/comments.md +101 -0
  7. package/dist/agent-src/commands/fix.md +5 -2
  8. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +65 -0
  9. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +68 -0
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +68 -0
  11. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +62 -0
  12. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +59 -0
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +8 -3
  14. package/dist/agent-src/contexts/execution/subagent-response-contract.md +56 -0
  15. package/dist/agent-src/contexts/execution/subagent-routing.md +57 -0
  16. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-steering.md +71 -0
  18. package/dist/agent-src/contexts/execution/verify-budget.md +50 -0
  19. package/dist/agent-src/rules/brand-source-of-truth.md +10 -0
  20. package/dist/agent-src/rules/delegation-policy.md +85 -0
  21. package/dist/agent-src/rules/design-fidelity.md +98 -0
  22. package/dist/agent-src/rules/domain-safety-pii.md +22 -0
  23. package/dist/agent-src/rules/legal-safety-floor.md +182 -0
  24. package/dist/agent-src/rules/lethal-trifecta-guard.md +25 -22
  25. package/dist/agent-src/rules/output-discipline.md +85 -0
  26. package/dist/agent-src/rules/telegraph-speak.md +4 -1
  27. package/dist/agent-src/rules/token-budget-discipline.md +94 -0
  28. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +102 -15
  29. package/dist/agent-src/scripts/update_roadmap_progress.ts +30 -3
  30. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -5
  31. package/dist/agent-src/skills/ai-council/SKILL.md +28 -1
  32. package/dist/agent-src/skills/blade-ui/SKILL.md +9 -0
  33. package/dist/agent-src/skills/contract-review/SKILL.md +125 -0
  34. package/dist/agent-src/skills/contract-review/evals/triggers.json +17 -0
  35. package/dist/agent-src/skills/dashboard-design/SKILL.md +21 -5
  36. package/dist/agent-src/skills/design-intelligence/SKILL.md +96 -0
  37. package/dist/agent-src/skills/design-review/SKILL.md +83 -0
  38. package/dist/agent-src/skills/design-system-capture/SKILL.md +234 -0
  39. package/dist/agent-src/skills/design-tokens/SKILL.md +1 -0
  40. package/dist/agent-src/skills/dpa-review/SKILL.md +130 -0
  41. package/dist/agent-src/skills/dpa-review/evals/triggers.json +17 -0
  42. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +15 -4
  43. package/dist/agent-src/skills/fe-design/SKILL.md +98 -16
  44. package/dist/agent-src/skills/flux/SKILL.md +8 -0
  45. package/dist/agent-src/skills/iconography/SKILL.md +1 -0
  46. package/dist/agent-src/skills/laravel/SKILL.md +1 -0
  47. package/dist/agent-src/skills/legal-intake-triage/SKILL.md +128 -0
  48. package/dist/agent-src/skills/legal-intake-triage/evals/triggers.json +17 -0
  49. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +101 -0
  50. package/dist/agent-src/skills/legal-practice-profile/evals/triggers.json +17 -0
  51. package/dist/agent-src/skills/livewire/SKILL.md +8 -0
  52. package/dist/agent-src/skills/nda-triage/SKILL.md +114 -0
  53. package/dist/agent-src/skills/nda-triage/evals/triggers.json +17 -0
  54. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +21 -9
  55. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +3 -1
  56. package/dist/agent-src/skills/recursive-verification/SKILL.md +186 -0
  57. package/dist/agent-src/skills/roadmap-management/SKILL.md +17 -0
  58. package/dist/agent-src/skills/skill-writing/SKILL.md +18 -0
  59. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +11 -0
  60. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +17 -7
  61. package/dist/agent-src/skills/typography-system/SKILL.md +14 -0
  62. package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
  63. package/dist/agent-src/templates/agent-settings.md +64 -19
  64. package/dist/discovery/deprecation-report.md +1 -1
  65. package/dist/discovery/discovery-manifest.json +362 -43
  66. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  67. package/dist/discovery/discovery-manifest.summary.md +13 -5
  68. package/dist/discovery/orphan-report.md +1 -1
  69. package/dist/discovery/packs.json +56 -11
  70. package/dist/discovery/trust-report.md +7 -5
  71. package/dist/discovery/workspaces.json +38 -7
  72. package/dist/mcp/registry-manifest.json +3 -3
  73. package/dist/router.json +1 -1
  74. package/dist/server/routes/wizard.js +4 -3
  75. package/dist/server/routes/wizard.js.map +1 -1
  76. package/dist/server/schemas/settings.js +23 -0
  77. package/dist/server/schemas/settings.js.map +1 -1
  78. package/dist/ui/assets/{index-CoQo90sz.js → index-UjcTG_9O.js} +2 -2
  79. package/dist/ui/assets/index-UjcTG_9O.js.map +1 -0
  80. package/dist/ui/index.html +1 -1
  81. package/docs/RELEASE_STORY_TEMPLATE.md +37 -0
  82. package/docs/architecture.md +4 -4
  83. package/docs/benchmark.md +46 -0
  84. package/docs/catalog.md +18 -6
  85. package/docs/command-flows.md +7 -1
  86. package/docs/contracts/ai-council-config.md +17 -10
  87. package/docs/contracts/audit-log-v1.md +7 -0
  88. package/docs/contracts/auto-orchestration-v1.md +70 -0
  89. package/docs/contracts/capability-boundary.md +64 -0
  90. package/docs/contracts/command-clusters.md +1 -1
  91. package/docs/contracts/discovery-manifest.schema.json +2 -2
  92. package/docs/contracts/package-self-orientation.md +3 -0
  93. package/docs/contracts/subagent-boundary.md +54 -0
  94. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +32 -0
  95. package/docs/decisions/ADR-020-global-only-consumer-scope.md +13 -6
  96. package/docs/decisions/ADR-049-configuration-trust-boundary.md +11 -1
  97. package/docs/decisions/ADR-093-ai-council-config-user-global.md +10 -4
  98. package/docs/decisions/ADR-104-ai-council-config-global-only.md +119 -0
  99. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +102 -0
  100. package/docs/decisions/ADR-106-recursive-verification-benchmark-gate.md +126 -0
  101. package/docs/decisions/ADR-107-legal-domain-pack-adoption.md +70 -0
  102. package/docs/decisions/ADR-108-open-source-forever-no-commercial-tier.md +46 -0
  103. package/docs/decisions/INDEX.md +5 -0
  104. package/docs/getting-started.md +2 -2
  105. package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +53 -0
  106. package/docs/guidelines/agent-infra/domain-pack-architecture.md +56 -0
  107. package/docs/guidelines/design-antipatterns-triggers.json +17 -0
  108. package/docs/guidelines/design-antipatterns.md +170 -0
  109. package/docs/guidelines/design-modes.md +97 -0
  110. package/package.json +1 -1
  111. package/src/config/agent-settings.template.yml +103 -3
  112. package/src/config/discovery/packs.yml +24 -0
  113. package/src/config/discovery/workspaces.yml +7 -0
  114. package/src/scripts/_lib/auto_dispatch.ts +95 -0
  115. package/src/scripts/_lib/host_capability.ts +59 -0
  116. package/src/scripts/_lib/orchestration_gate.ts +98 -0
  117. package/src/scripts/_lib/subagent_bundle.ts +154 -0
  118. package/src/scripts/_lib/subagent_response.ts +82 -0
  119. package/src/scripts/_lib/subagent_routing.ts +81 -0
  120. package/src/scripts/_lib/subagent_spawn.ts +97 -0
  121. package/src/scripts/_lib/subagent_steering.ts +64 -0
  122. package/src/scripts/_lib/verify_budget.ts +54 -0
  123. package/src/scripts/ai_council/advisors.ts +4 -2
  124. package/src/scripts/ai_council/clients.ts +7 -1
  125. package/src/scripts/ai_council/config.ts +22 -17
  126. package/src/scripts/bench_ab_v2_run.ts +127 -0
  127. package/src/scripts/bench_ab_v2_stats.ts +39 -0
  128. package/src/scripts/check_council_config_location.ts +6 -5
  129. package/src/scripts/check_trunk_drift.ts +67 -0
  130. package/src/scripts/lint_design_quality.ts +321 -0
  131. package/src/scripts/lint_discovery_vocabulary.ts +6 -0
  132. package/src/scripts/lint_framework_leakage_allowlist.json +21 -0
  133. package/src/scripts/lint_legal_pack.ts +197 -0
  134. package/src/scripts/lint_output_slop.ts +223 -0
  135. package/src/scripts/lint_pack_risk_class.ts +79 -0
  136. package/src/scripts/probe_projection_fidelity.ts +7 -1
  137. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  138. package/src/scripts/schemas/skill.schema.json +5 -0
  139. package/src/scripts/skill_linter.ts +3 -0
  140. package/dist/ui/assets/index-CoQo90sz.js.map +0 -1
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Verification-budget model (Phase 4).
3
+ *
4
+ * Pure, no-I/O. Decides HOW a delegated sub-task's output is verified so the
5
+ * cross-model judge Iron Law and verify-before-complete are preserved WITHOUT
6
+ * double-costing every trivial delegation. Contract:
7
+ * `src/agent-src/contexts/execution/verify-budget.md`.
8
+ *
9
+ * - Trivial (below the change-size floor, read-only / no file writes) →
10
+ * `deterministic` verification (diff + dry-run + structural checks, no LLM).
11
+ * - Otherwise → `judge` (full cross-model judge per subagent-orchestration).
12
+ * - A required verification that did not run is a SURFACED safety gap, never a
13
+ * silent pass.
14
+ */
15
+
16
+ export type VerifyMode = 'deterministic' | 'judge' | 'none';
17
+
18
+ /** Max changed-line count (inclusive) that still counts as trivial. */
19
+ export const TRIVIAL_CHANGE_FLOOR = 20;
20
+
21
+ export interface VerifyInputs {
22
+ /** Changed lines the sub-task produced. */
23
+ change_size: number;
24
+ /** Did the sub-task write/modify files? */
25
+ file_writes: boolean;
26
+ /** Was the sub-task read-only (query/analysis, no mutation)? */
27
+ read_only: boolean;
28
+ }
29
+
30
+ /**
31
+ * Select the verification mode for a completed sub-task.
32
+ * Trivial + non-mutating → deterministic; anything else → judge.
33
+ */
34
+ export function selectVerifyMode(inp: VerifyInputs): VerifyMode {
35
+ if (inp.read_only) return 'deterministic';
36
+ const trivial = inp.change_size <= TRIVIAL_CHANGE_FLOOR && !inp.file_writes;
37
+ return trivial ? 'deterministic' : 'judge';
38
+ }
39
+
40
+ /**
41
+ * Detect a verification safety gap: a mode was required but the recorded mode
42
+ * was `none`. Returns a human-readable gap string, or null when clean.
43
+ * The orchestrator surfaces a non-null result — it is never a silent pass.
44
+ */
45
+ export function verificationGap(required: VerifyMode, recorded: VerifyMode): string | null {
46
+ if (required === 'none') return null;
47
+ if (recorded === 'none') {
48
+ return `verification gap: '${required}' required but none recorded`;
49
+ }
50
+ if (required === 'judge' && recorded === 'deterministic') {
51
+ return `verification gap: non-trivial change verified deterministically (judge required)`;
52
+ }
53
+ return null;
54
+ }
@@ -2,8 +2,10 @@
2
2
  * Thinking-style advisors — replace-mode call planning (Phase 6).
3
3
  *
4
4
  * TypeScript twin of `src/scripts/ai_council/advisors.py` (ADR-200 —
5
- * Python→TS migration, Phase 1). When `agents/settings/.ai-council.yml`
6
- * enables an advisor (e.g. `contrarian` bound to `member: anthropic`), the
5
+ * Python→TS migration, Phase 1). When the user-global
6
+ * `~/.event4u/agent-config/settings/.ai-council.yml` (always global —
7
+ * ADR-104) enables an advisor (e.g. `contrarian` bound to
8
+ * `member: anthropic`), the
7
9
  * orchestrator REPLACES the matching plain-member call with an
8
10
  * advisor-persona call on the same provider. Same total call count as a
9
11
  * plain run; bounded extra cost beyond the persona-prompt token delta.
@@ -287,12 +287,18 @@ function _curlJsonPost(url: string, extraHeaders: string[], body: unknown): unkn
287
287
  for (const h of extraHeaders) {
288
288
  args.push('-H', h);
289
289
  }
290
+ // `--connect-timeout` fast-fails a dead host; `--max-time` lets curl abort
291
+ // itself cleanly (surfacing a real `curl exited` error with stderr) ~10s
292
+ // before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
293
+ // A full 16k-token generation legitimately runs several minutes, so the
294
+ // ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
295
+ args.push('--connect-timeout', '30', '--max-time', '290');
290
296
  args.push('-w', '\n%{http_code}', '--data-binary', '@-');
291
297
  const r = spawnSync('curl', args, {
292
298
  input: JSON.stringify(body),
293
299
  encoding: 'utf8',
294
300
  maxBuffer: 64 * 1024 * 1024,
295
- timeout: 120_000,
301
+ timeout: 300_000,
296
302
  });
297
303
  if (r.error) {
298
304
  throw new Error(`curl spawn failed: ${(r.error as Error).message}`);
@@ -6,8 +6,9 @@
6
6
  * surface and validation behaviour byte-for-byte: same exported
7
7
  * snake_case names, same defaults, same error messages, same precedence.
8
8
  *
9
- * Reads `agents/settings/.ai-council.yml` per the contract in
10
- * `docs/contracts/ai-council-config.md`. Replaces the fragmented
9
+ * Reads the user-global `~/.event4u/agent-config/settings/.ai-council.yml`
10
+ * per the contract in `docs/contracts/ai-council-config.md` (council
11
+ * config is ALWAYS user-global — ADR-104). Replaces the fragmented
11
12
  * `.agent-settings.yml` `ai_council` block (Phase 0 migration).
12
13
  *
13
14
  * Validation contract (8 rules, all enforced at load time):
@@ -93,7 +94,7 @@ export type PathLike = string;
93
94
 
94
95
  // ── error type ─────────────────────────────────────────────────────
95
96
 
96
- /** Raised when `agents/settings/.ai-council.yml` violates the schema. */
97
+ /** Raised when the user-global `.ai-council.yml` violates the schema. */
97
98
  export class CouncilConfigError extends Error {
98
99
  constructor(message: string) {
99
100
  super(message);
@@ -562,8 +563,10 @@ export const COUNCIL_CONFIG_USER_GLOBAL_REL = 'settings/.ai-council.yml';
562
563
 
563
564
  /**
564
565
  * Env var pinning the council config to an explicit absolute path, ahead
565
- * of the project → user-global search. Mirrors `EVENT4U_CONFIG_HOME` but
566
- * targets the config file itself (tests / power users).
566
+ * of the user-global default. Mirrors `EVENT4U_CONFIG_HOME` but targets
567
+ * the config file itself (tests / power users). This is the ONLY escape
568
+ * from the user-global location — it is an explicit absolute path, never
569
+ * a "search the project" path.
567
570
  */
568
571
  export const COUNCIL_CONFIG_ENV = 'AI_COUNCIL_CONFIG';
569
572
 
@@ -598,14 +601,24 @@ function _isAbsoluteLikePython(p: string): boolean {
598
601
  /**
599
602
  * Resolve which `.ai-council.yml` the council reads.
600
603
  *
604
+ * **The council config is ALWAYS user-global.** It is a per-developer
605
+ * facility configured once and works in every project, worktree, and
606
+ * CWD — including consumer repos that carry no council file of their
607
+ * own. The project tree is NEVER searched for council config (ADR-104,
608
+ * superseding the project-local override kept by ADR-093).
609
+ *
601
610
  * Precedence (first match wins):
602
611
  *
603
612
  * 1. `$AI_COUNCIL_CONFIG` — explicit absolute override (tests / power
604
- * users). Honoured even when the target is absent.
605
- * 2. Project-local `<project_root>/agents/settings/.ai-council.yml`.
606
- * 3. User-global `~/.event4u/agent-config/settings/.ai-council.yml`
613
+ * users). Honoured even when the target is absent. This is an
614
+ * explicit path, not a project search.
615
+ * 2. User-global `~/.event4u/agent-config/settings/.ai-council.yml`
607
616
  * (with the legacy `~/.config/agent-config/` read-fallback).
608
617
  *
618
+ * `project_root` is accepted for signature stability with callers but is
619
+ * NOT consulted for config resolution — the council never reads
620
+ * `<project_root>/agents/settings/.ai-council.yml`.
621
+ *
609
622
  * Always returns a path (never `null`): when nothing exists yet it
610
623
  * returns the user-global write target.
611
624
  */
@@ -613,20 +626,12 @@ export function resolve_config_path(
613
626
  project_root: PathLike,
614
627
  options: { env?: user_global_paths.EnvMap | null } = {},
615
628
  ): PathLike {
629
+ void project_root; // intentionally unused — council config is always user-global (ADR-104).
616
630
  const env_map = options.env != null ? options.env : process.env;
617
631
  const override = env_map[COUNCIL_CONFIG_ENV];
618
632
  if (override) {
619
633
  return _expanduser(override);
620
634
  }
621
- const project_path = path.join(
622
- project_root,
623
- 'agents',
624
- 'settings',
625
- COUNCIL_CONFIG_RELNAME,
626
- );
627
- if (fs.existsSync(project_path)) {
628
- return project_path;
629
- }
630
635
  const found = user_global_paths.resolve_with_fallback(
631
636
  COUNCIL_CONFIG_USER_GLOBAL_REL,
632
637
  { env: options.env ?? null },
@@ -67,14 +67,20 @@ type Dict = Record<string, unknown>;
67
67
  interface ArmSpec {
68
68
  setting_sources: string | null;
69
69
  inject: string | null; // None | "rdp" | "placebo"
70
+ recursive?: boolean; // ADR-106 D₂ arm: depth-bounded attempt→critic→re-attempt loop
70
71
  }
71
72
 
72
73
  // Arm -> (setting_sources, inject) where inject ∈ {None, "rdp", "placebo"}.
74
+ // `package-recursive` (ADR-106's D₂) reuses the real-plugin `package` config but
75
+ // runs the recursion loop instead of a single --print. It only executes when
76
+ // explicitly selected (`--arms package-recursive`); the four default arms are
77
+ // untouched, so existing runs and their golden-parity outputs are unchanged.
73
78
  const ARMS: Record<string, ArmSpec> = {
74
79
  vanilla: { setting_sources: 'project,local', inject: null },
75
80
  package: { setting_sources: null, inject: null },
76
81
  'package-rdp': { setting_sources: null, inject: 'rdp' },
77
82
  placebo: { setting_sources: 'project,local', inject: 'placebo' },
83
+ 'package-recursive': { setting_sources: null, inject: null, recursive: true },
78
84
  };
79
85
 
80
86
  /**
@@ -165,10 +171,14 @@ interface RunOneOpts {
165
171
  timeout: number;
166
172
  placebo_chars: number;
167
173
  sp_dir: string;
174
+ max_depth?: number | null; // recursion arm only — hard cap on correction rounds (default 1)
168
175
  }
169
176
 
170
177
  export function run_one(task: Dict, arm: string, opts: RunOneOpts): Dict {
171
178
  const spec = ARMS[arm] as ArmSpec;
179
+ if (spec.recursive) {
180
+ return run_one_recursive(task, opts);
181
+ }
172
182
  const [clone, fixture] = reset_fixture(task);
173
183
  const sp_text = injected_text(spec.inject, opts.placebo_chars);
174
184
  let sp_file: string | null = null;
@@ -198,6 +208,123 @@ export function run_one(task: Dict, arm: string, opts: RunOneOpts): Dict {
198
208
  };
199
209
  }
200
210
 
211
+ export interface RecursiveAttempt {
212
+ run: Dict;
213
+ score: scoring.ScoreResultV2;
214
+ output?: string; // pair-capture: changed-file contents at this depth (for human-preference judging)
215
+ }
216
+
217
+ /**
218
+ * ADR-106 D₂ arm — depth-bounded recursive self-verification.
219
+ *
220
+ * Loop: attempt → critic verdict → conditional re-attempt, depth = hard compute
221
+ * cap. The critic is the DETERMINISTIC v2 scorer (accept iff the attempt is both
222
+ * capability- AND discipline-passing) — cheapest critic, no extra model call,
223
+ * fully testable. A model-based / cross-vendor critic is a later option (see
224
+ * road-to-recursive-verification.md Phase 4).
225
+ *
226
+ * `attemptFn(depth, priorVerdict)` is an injectable seam: the default runs the
227
+ * real `package`-config agent (prior verdict threaded as the system prompt on
228
+ * re-attempts) and scores it; tests inject a scripted attemptFn to exercise the
229
+ * loop control flow with NO live model call.
230
+ *
231
+ * Verifies the loop's CONTROL FLOW (stop conditions, depth cap, verdict
232
+ * threading). Whether recursion actually lifts capability/discipline is the
233
+ * empirical question the live `bench:ab` run answers — not asserted here.
234
+ */
235
+ export function run_one_recursive(
236
+ task: Dict,
237
+ opts: RunOneOpts,
238
+ attemptFn?: (depth: number, priorVerdict: string | null) => RecursiveAttempt,
239
+ onAttempt?: (depth: number, attempt: RecursiveAttempt) => void,
240
+ ): Dict {
241
+ const maxDepth = Math.max(0, opts.max_depth ?? 1);
242
+ const makeAttempt =
243
+ attemptFn ??
244
+ ((depth: number, priorVerdict: string | null): RecursiveAttempt => {
245
+ const [clone, fixture] = reset_fixture(task);
246
+ let sp_file: string | null = null;
247
+ if (priorVerdict) {
248
+ sp_file = path.join(opts.sp_dir, `.sp-recursive-${depth}.txt`);
249
+ fs.writeFileSync(sp_file, priorVerdict, { encoding: 'utf-8' });
250
+ }
251
+ const run = v1.run_live(task, clone, opts.timeout, {
252
+ sysprompt_file: sp_file,
253
+ setting_sources: (ARMS['package'] as ArmSpec).setting_sources,
254
+ max_budget: opts.max_budget,
255
+ model: opts.model,
256
+ }) as unknown as Dict;
257
+ const score = scoring.score_task_v2(task, {
258
+ fixture_root: fixture,
259
+ clone_root: clone,
260
+ transcript: String(run['transcript'] ?? ''),
261
+ });
262
+ // Capture the attempt's output (changed-file contents) for the
263
+ // human-preference pair, before the next depth resets the clone.
264
+ const output = score.files_changed
265
+ .map((f: string) => {
266
+ try {
267
+ return `--- ${f} ---\n${fs.readFileSync(path.join(clone, f), 'utf-8')}`;
268
+ } catch {
269
+ return `--- ${f} --- (unreadable)`;
270
+ }
271
+ })
272
+ .join('\n\n')
273
+ .slice(0, 4000);
274
+ return { run, score, output };
275
+ });
276
+
277
+ const accepts = (s: scoring.ScoreResultV2): boolean => Boolean(s.capability_pass) && Boolean(s.discipline_pass);
278
+
279
+ let depth = 0;
280
+ let prior: RecursiveAttempt | null = null;
281
+ let attempt = makeAttempt(0, null);
282
+ onAttempt?.(0, attempt);
283
+ let stop_reason: string;
284
+ let last_verdict_len = 0;
285
+ for (;;) {
286
+ const s = attempt.score;
287
+ if (accepts(s)) {
288
+ stop_reason = 'accept';
289
+ break;
290
+ }
291
+ if (depth >= maxDepth) {
292
+ stop_reason = 'max_depth';
293
+ break;
294
+ }
295
+ if (
296
+ prior !== null &&
297
+ s.discipline_score === prior.score.discipline_score &&
298
+ Boolean(s.capability_pass) === Boolean(prior.score.capability_pass)
299
+ ) {
300
+ stop_reason = 'no_progress';
301
+ break;
302
+ }
303
+ depth += 1;
304
+ const verdict =
305
+ `Prior attempt (depth ${depth - 1}) did not pass: ` +
306
+ `capability=${s.capability_pass}, discipline=${s.discipline_score}. ` +
307
+ `Revise to satisfy the unmet acceptance criteria; change only what the task requires.`;
308
+ last_verdict_len = verdict.length;
309
+ prior = attempt;
310
+ attempt = makeAttempt(depth, verdict);
311
+ onAttempt?.(depth, attempt);
312
+ }
313
+
314
+ const { run, score } = attempt;
315
+ return {
316
+ errored: _pyBool(run['errored']),
317
+ reason: run['reason'] ?? null,
318
+ capability_pass: score.capability_pass,
319
+ discipline_score: new PyFloat(score.discipline_score),
320
+ discipline_pass: score.discipline_pass,
321
+ metrics: trajectory_metrics(run, score),
322
+ injected_chars: last_verdict_len,
323
+ depth_reached: depth,
324
+ stop_reason,
325
+ };
326
+ }
327
+
201
328
  export function main(argv: string[] | null = null): number {
202
329
  const args = parse_args(argv ?? process.argv.slice(2));
203
330
 
@@ -41,6 +41,11 @@ const COMPARISONS: Array<[string, string, string]> = [
41
41
  ['package', 'vanilla', 'package lift'],
42
42
  ['package-rdp', 'package', 'RDP lift'],
43
43
  ['package', 'placebo', 'attribution (content vs length)'],
44
+ // ADR-106: recursion's NOVEL lift over rules-only (D₂ − D₁). The analyse()
45
+ // loop is arm-guarded (`arms.includes(t) && arms.includes(b)`), so this row
46
+ // renders ONLY when a `package-recursive` arm is present — existing runs
47
+ // (no such arm) are byte-identical, golden parity preserved.
48
+ ['package-recursive', 'package', 'recursion novel lift (D₂ − D₁)'],
44
49
  ];
45
50
 
46
51
  // ── CPython math.erf / math.comb ports ────────────────────────────────────
@@ -333,6 +338,40 @@ export function compare(records: Dict[], arm_t: string, arm_b: string): Dict {
333
338
  };
334
339
  }
335
340
 
341
+ /**
342
+ * Recursion's NOVEL discipline lift over rules-only (ADR-106).
343
+ *
344
+ * Compares the `package-recursive` arm (D₂ = rules + recursion) against
345
+ * `package` (D₁ = rules only) via the existing pure `compare()`, so recursion's
346
+ * marginal lift `D₂ − D₁` is a measured fact, never an assumed redundancy.
347
+ *
348
+ * ADDITIVE: does NOT touch the rendered COMPARISONS path — existing CLI output
349
+ * (and its golden-parity tests) are unaffected. Returns `null` when no
350
+ * `package-recursive` arm is present in the records, so a future renderer can
351
+ * skip the block cleanly for non-recursion runs.
352
+ *
353
+ * NOTE (Phase 2b live-integration, NOT done here): wiring this into the rendered
354
+ * stats output (a guarded COMPARISONS entry + a golden-parity fixture refresh)
355
+ * and producing the `package-recursive` arm itself (a multi-call recursion loop
356
+ * the single-`--print` runner does not yet support) require the live bench and
357
+ * are deferred to the supervised Phase 3 session.
358
+ */
359
+ export const RECURSIVE_ARM = 'package-recursive';
360
+ export const RECURSIVE_BASELINE_ARM = 'package';
361
+
362
+ export function recursiveNovelLift(records: Dict[]): Dict | null {
363
+ const hasRecursion = records.some((rec) => {
364
+ const runs = _dictOr(rec['arms'])[RECURSIVE_ARM];
365
+ return Array.isArray(runs) && runs.length > 0;
366
+ });
367
+ if (!hasRecursion) return null;
368
+ return {
369
+ label: 'recursion novel lift (D₂ − D₁)',
370
+ arms: [RECURSIVE_ARM, RECURSIVE_BASELINE_ARM],
371
+ ...compare(records, RECURSIVE_ARM, RECURSIVE_BASELINE_ARM),
372
+ };
373
+ }
374
+
336
375
  export function mean_tokens_by_arm(records: Dict[], arms: string[]): Dict {
337
376
  // Mean total tokens per arm over non-errored runs (the cost axis, L10).
338
377
  // Truncated/errored runs are excluded — their token count is capped by the
@@ -9,11 +9,12 @@
9
9
  * and the same `<!-- council-config-allowed -->` escape pragma. No behaviour
10
10
  * changes — latent bugs replicated.
11
11
  *
12
- * Per ADR-093 the council reads a dedicated `.ai-council.yml` — resolved
13
- * project-local (`agents/settings/.ai-council.yml`) first, else user-global
14
- * (`~/.event4u/agent-config/settings/.ai-council.yml`). Keys are top-level in
15
- * that file; the legacy `ai_council.*` block under `.agent-settings.yml` was
16
- * removed in Phase 0.
12
+ * Per ADR-104 (superseding ADR-093) the council reads a dedicated
13
+ * `.ai-council.yml` resolved ALWAYS from the user-global location
14
+ * (`~/.event4u/agent-config/settings/.ai-council.yml`) — the project tree
15
+ * is never searched (the only escape is the explicit `$AI_COUNCIL_CONFIG`
16
+ * path). Keys are top-level in that file; the legacy `ai_council.*` block
17
+ * under `.agent-settings.yml` was removed in Phase 0.
17
18
  *
18
19
  * What it flags, in the council command/skill surfaces + the config contract:
19
20
  *
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Pre-PR trunk-drift check (road-to-product-clarity Phase 4b).
3
+ *
4
+ * Fails when the current branch is behind origin/main — i.e. origin/main is NOT
5
+ * an ancestor of HEAD — so a PR is never opened on a stale base (the recurring
6
+ * divergence this session hit). Advisory by default: run it in the /create-pr
7
+ * pre-flight or wire into CI per your branch-protection policy.
8
+ *
9
+ * git merge-base --is-ancestor origin/main HEAD → exit 0 = up to date
10
+ *
11
+ * Pure core (`driftVerdict`) is unit-testable without git.
12
+ */
13
+
14
+ import { execFileSync } from 'node:child_process';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import * as path from 'node:path';
17
+
18
+ export type DriftInput = {
19
+ /** True when origin/main is an ancestor of HEAD (branch contains trunk tip). */
20
+ trunkIsAncestor: boolean;
21
+ /** Commits HEAD is behind origin/main (0 when up to date). */
22
+ behind: number;
23
+ };
24
+
25
+ export type DriftVerdict = { ok: boolean; message: string };
26
+
27
+ /** Pure decision — no git. */
28
+ export function driftVerdict(i: DriftInput): DriftVerdict {
29
+ if (i.trunkIsAncestor) {
30
+ return { ok: true, message: '✅ branch is up to date with origin/main' };
31
+ }
32
+ return {
33
+ ok: false,
34
+ message: `❌ branch is ${i.behind} commit(s) behind origin/main — rebase/merge before opening a PR`,
35
+ };
36
+ }
37
+
38
+ function _git(args: string[]): string {
39
+ return execFileSync('git', args, { encoding: 'utf-8' }).trim();
40
+ }
41
+
42
+ export function main(): number {
43
+ let input: DriftInput;
44
+ try {
45
+ _git(['fetch', 'origin', 'main', '--quiet']);
46
+ let isAncestor = true;
47
+ try {
48
+ execFileSync('git', ['merge-base', '--is-ancestor', 'origin/main', 'HEAD'], { stdio: 'ignore' });
49
+ } catch {
50
+ isAncestor = false;
51
+ }
52
+ const behind = Number(_git(['rev-list', '--count', 'HEAD..origin/main']) || '0');
53
+ input = { trunkIsAncestor: isAncestor, behind };
54
+ } catch (e) {
55
+ process.stdout.write(`⚠️ trunk-drift check skipped (no origin/main or git error): ${String(e)}\n`);
56
+ return 0; // advisory — never block on a missing remote
57
+ }
58
+ const v = driftVerdict(input);
59
+ process.stdout.write(`${v.message}\n`);
60
+ return v.ok ? 0 : 1;
61
+ }
62
+
63
+ const _HERE = fileURLToPath(import.meta.url);
64
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
65
+ process.exit(main());
66
+ }
67
+ void _HERE;