@event4u/agent-config 8.11.0 → 8.12.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 (67) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/CHANGELOG.md +49 -279
  4. package/README.md +2 -2
  5. package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
  6. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +106 -94
  7. package/dist/agent-src/rules/architecture.md +6 -51
  8. package/dist/agent-src/rules/code-comment-discipline.md +10 -94
  9. package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
  10. package/dist/agent-src/rules/improve-before-implement.md +7 -66
  11. package/dist/agent-src/rules/no-roadmap-references.md +13 -77
  12. package/dist/agent-src/rules/persona-governance.md +5 -47
  13. package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
  14. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
  15. package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
  16. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +83 -0
  17. package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
  18. package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
  19. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  20. package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
  21. package/dist/discovery/deprecation-report.md +1 -1
  22. package/dist/discovery/discovery-manifest.json +14 -14
  23. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  24. package/dist/discovery/discovery-manifest.summary.md +1 -1
  25. package/dist/discovery/orphan-report.md +1 -1
  26. package/dist/discovery/packs.json +2 -2
  27. package/dist/discovery/trust-report.md +1 -1
  28. package/dist/discovery/workspaces.json +2 -2
  29. package/dist/mcp/registry-manifest.json +1 -1
  30. package/dist/router.json +1 -1
  31. package/docs/CLAIMS.md +30 -2
  32. package/docs/architecture.md +1 -1
  33. package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
  34. package/docs/benchmark.md +39 -0
  35. package/docs/contracts/ai-council-config.md +9 -4
  36. package/docs/contracts/persona-schema.md +49 -0
  37. package/docs/contracts/provider-lifecycle.md +29 -4
  38. package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
  39. package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
  40. package/docs/decisions/INDEX.md +2 -0
  41. package/docs/design/council-vs-solo-baseline.md +92 -0
  42. package/docs/design/utilization-window-criteria.md +51 -0
  43. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
  44. package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  45. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
  46. package/docs/guidelines/code-clarity.md +86 -1
  47. package/docs/maintainers/branch-protection.md +87 -0
  48. package/docs/maintainers/system-map.md +85 -0
  49. package/docs/proof.md +14 -3
  50. package/package.json +1 -1
  51. package/src/config/agent-settings.template.yml +9 -0
  52. package/src/scripts/_cli/cmd_doctor.ts +95 -0
  53. package/src/scripts/_lib/knowledge_global.ts +44 -0
  54. package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
  55. package/src/scripts/ai_council/orchestrator.ts +188 -1
  56. package/src/scripts/bench_persona_placebo.ts +308 -0
  57. package/src/scripts/bench_quality_rerun.ts +357 -0
  58. package/src/scripts/build_proof.ts +9 -0
  59. package/src/scripts/check_artefact_count_messaging.ts +56 -0
  60. package/src/scripts/check_knowledge_cards.ts +33 -4
  61. package/src/scripts/check_portability.ts +1 -0
  62. package/src/scripts/complexity_report.ts +776 -0
  63. package/src/scripts/consumer_matrix.ts +251 -12
  64. package/src/scripts/council_cli.ts +56 -1
  65. package/src/scripts/explain_run.ts +593 -0
  66. package/src/scripts/knowledge_global_cli.ts +234 -45
  67. package/src/scripts/rule_backlinks.ts +0 -0
@@ -4,15 +4,25 @@
4
4
  *
5
5
  * Exercises the PUBLISHED TARBALL the way a consumer does: pack → fresh
6
6
  * global install (isolated prefix) → `init` into a fresh project → doctor →
7
- * conformance → MCP stdio handshake → hook health → projection presence →
8
- * uninstall. Optional upgrade leg: install the last published minor from the
9
- * registry, then upgrade to the packed tarball and assert doctor stays green.
7
+ * conformance → MCP stdio handshake → hook health → hook lifecycle →
8
+ * projection presence → uninstall. Optional upgrade leg: install the last
9
+ * published minor from the registry, then upgrade to the packed tarball and
10
+ * assert doctor stays green.
10
11
  *
11
12
  * Why this exists: the release-PR gate skips source-level matrices (soundly —
12
13
  * a version-bump diff cannot regress source behaviour), but nothing between
13
14
  * merge and tag exercised the tarball shape. Two published minors shipped
14
15
  * without `src/install/` while published code imported from it; `tsx` was
15
16
  * missing from the published package; each manifested only at publish time.
17
+ * A third bug (missing `dist/install/rule_scope.js`) shipped past every leg
18
+ * because none of them fired a REAL installed hook command — `conformance`'s
19
+ * dispatcher smoke calls `dispatch_hook.ts` directly via `tsx`, bypassing the
20
+ * `agent-config` binary (and its ESM import graph) entirely. The `hooks`
21
+ * (static doctor) and `hook-lifecycle` (this file's live leg, below) legs
22
+ * close that gap: the latter runs the EXACT shell command Claude installed in
23
+ * `~/.claude/settings.json`, so a crash anywhere in the binary's load path —
24
+ * including a missing compiled sibling module — fails the leg with the
25
+ * culprit's stderr, not silently at publish time.
16
26
  * Every leg here asserts exit code + an expected artifact, no LLM calls.
17
27
  *
18
28
  * Usage:
@@ -30,7 +40,9 @@ import { spawnSync, spawn } from 'node:child_process';
30
40
  import * as fs from 'node:fs';
31
41
  import * as os from 'node:os';
32
42
  import * as path from 'node:path';
33
- import { fileURLToPath } from 'node:url';
43
+ import { fileURLToPath, pathToFileURL } from 'node:url';
44
+
45
+ import { MANAGED_SIGNATURE } from './_lib/claude_settings_hooks.js';
34
46
 
35
47
  const _HERE = fileURLToPath(import.meta.url);
36
48
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -63,7 +75,7 @@ function log(msg: string): void {
63
75
  function run(
64
76
  cmd: string,
65
77
  args: string[],
66
- opts: { cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number } = {},
78
+ opts: { cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number; input?: string } = {},
67
79
  ): { status: number; stdout: string; stderr: string } {
68
80
  const r = spawnSync(cmd, args, {
69
81
  cwd: opts.cwd ?? REPO_ROOT,
@@ -71,6 +83,7 @@ function run(
71
83
  encoding: 'utf8',
72
84
  timeout: opts.timeoutMs ?? 600_000,
73
85
  maxBuffer: 32 * 1024 * 1024,
86
+ input: opts.input,
74
87
  });
75
88
  return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
76
89
  }
@@ -216,6 +229,210 @@ function legHooks(ctx: Ctx): string {
216
229
  return 'hooks:doctor green';
217
230
  }
218
231
 
232
+ // ── hook-lifecycle leg ─────────────────────────────────────────────
233
+ //
234
+ // `hooks:doctor` (above) is a read-only static report — it never runs
235
+ // anything. `conformance`'s dispatcher smoke calls `dispatch_hook.ts`
236
+ // directly via `tsx`, bypassing the installed `agent-config` binary (and
237
+ // its ESM import graph) entirely. Neither exercises what a real Claude
238
+ // Code SessionStart/PreToolUse/PostToolUse/Stop event actually invokes:
239
+ // the exact shell command the installer registered in
240
+ // `~/.claude/settings.json`. This leg does — plus a deterministic
241
+ // completeness check on the compiled entrypoint's module graph, so a
242
+ // missing sibling `.js` (the historical `dist/install/rule_scope.js` bug)
243
+ // fails with the exact culprit instead of a live crash.
244
+
245
+ interface HookCommandEntry {
246
+ /** Native platform event name, e.g. `SessionStart`, `PreToolUse`. */
247
+ nativeEvent: string;
248
+ /** The exact managed shell command string installed for this event. */
249
+ command: string;
250
+ }
251
+
252
+ /**
253
+ * Read every agent-config-managed hook command out of an installed Claude
254
+ * settings file, keyed by native event. Mirrors the shape
255
+ * `ensure_managed_hooks` (src/scripts/_lib/claude_settings_hooks.ts) writes;
256
+ * a group is "managed" iff one of its commands contains `MANAGED_SIGNATURE`
257
+ * — the same identity check the installer itself uses, so this can never
258
+ * drift from what actually got registered.
259
+ */
260
+ export function readInstalledClaudeHookCommands(settingsPath: string): HookCommandEntry[] {
261
+ if (!fs.existsSync(settingsPath)) return [];
262
+ const raw = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
263
+ hooks?: Record<string, Array<{ hooks?: Array<{ command?: unknown }> }>>;
264
+ };
265
+ const hooks = raw.hooks ?? {};
266
+ const out: HookCommandEntry[] = [];
267
+ for (const [nativeEvent, groups] of Object.entries(hooks)) {
268
+ if (!Array.isArray(groups)) continue;
269
+ for (const group of groups) {
270
+ for (const h of group.hooks ?? []) {
271
+ if (typeof h.command === 'string' && h.command.includes(MANAGED_SIGNATURE)) {
272
+ out.push({ nativeEvent, command: h.command });
273
+ }
274
+ }
275
+ }
276
+ }
277
+ return out;
278
+ }
279
+
280
+ /** A representative Claude Code event envelope, shaped per native event. */
281
+ export function buildHookEnvelope(nativeEvent: string, projectDir: string): string {
282
+ const base = {
283
+ session_id: 'consumer-matrix-fixture',
284
+ transcript_path: '',
285
+ cwd: projectDir,
286
+ hook_event_name: nativeEvent,
287
+ };
288
+ switch (nativeEvent) {
289
+ case 'PreToolUse':
290
+ return JSON.stringify({
291
+ ...base,
292
+ tool_name: 'Read',
293
+ tool_input: { file_path: path.join(projectDir, 'README.md') },
294
+ });
295
+ case 'PostToolUse':
296
+ return JSON.stringify({
297
+ ...base,
298
+ tool_name: 'Read',
299
+ tool_input: { file_path: path.join(projectDir, 'README.md') },
300
+ tool_response: { success: true },
301
+ });
302
+ case 'Stop':
303
+ return JSON.stringify({ ...base, stop_hook_active: false });
304
+ case 'UserPromptSubmit':
305
+ return JSON.stringify({ ...base, prompt: 'consumer-matrix smoke prompt' });
306
+ case 'SessionEnd':
307
+ return JSON.stringify({ ...base, reason: 'exit' });
308
+ case 'SessionStart':
309
+ default:
310
+ return JSON.stringify({ ...base, source: 'startup' });
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Run one installed hook command exactly as the platform would — a shell
316
+ * invocation with the event envelope piped to stdin — and report whether it
317
+ * exited clean.
318
+ */
319
+ export function invokeHookCommand(
320
+ entry: HookCommandEntry,
321
+ opts: { cwd: string; env: NodeJS.ProcessEnv; projectDir: string; timeoutMs?: number },
322
+ ): { ok: boolean; status: number; stderrTail: string } {
323
+ const payload = buildHookEnvelope(entry.nativeEvent, opts.projectDir);
324
+ const r = run('bash', ['-c', entry.command], {
325
+ cwd: opts.cwd,
326
+ env: opts.env,
327
+ timeoutMs: opts.timeoutMs ?? 30_000,
328
+ input: payload,
329
+ });
330
+ const stderrTail = r.stderr.trim().split('\n').slice(-5).join(' | ');
331
+ return { ok: r.status === 0, status: r.status, stderrTail };
332
+ }
333
+
334
+ interface MissingModule {
335
+ from: string;
336
+ spec: string;
337
+ resolved: string;
338
+ }
339
+
340
+ /**
341
+ * Extract relative (`./`, `../`) ESM specifiers from a compiled `.js` file —
342
+ * static `from "..."`, bare `import "...";`, and dynamic `import("...")`.
343
+ * Regex, not a full parser: good enough for tsc's fixed output shape, and
344
+ * deliberately ignores bare package specifiers (`from 'commander'`) so the
345
+ * walk never leaves the package's own `dist/` tree.
346
+ */
347
+ export function parseRelativeSpecifiers(source: string): string[] {
348
+ const out = new Set<string>();
349
+ const patterns = [
350
+ /\bfrom\s*["']([^"']+)["']/g,
351
+ /^\s*import\s*["']([^"']+)["']\s*;/gm,
352
+ /\bimport\(\s*["']([^"']+)["']\s*\)/g,
353
+ ];
354
+ for (const re of patterns) {
355
+ let m: RegExpExecArray | null;
356
+ while ((m = re.exec(source)) !== null) {
357
+ const spec = m[1];
358
+ if (spec && (spec.startsWith('./') || spec.startsWith('../'))) out.add(spec);
359
+ }
360
+ }
361
+ return [...out];
362
+ }
363
+
364
+ /**
365
+ * Walk the compiled ESM module graph reachable from `entryFile`, following
366
+ * every relative import/export/dynamic-import specifier, and report any
367
+ * resolved target that does not exist on disk — the deterministic twin of
368
+ * "does this crash at require time", without spawning a process.
369
+ */
370
+ export function walkEsmModuleGraph(entryFile: string): { visited: string[]; missing: MissingModule[] } {
371
+ const visited = new Set<string>();
372
+ const missing: MissingModule[] = [];
373
+ const stack: string[] = [entryFile];
374
+ while (stack.length > 0) {
375
+ const file = stack.pop() as string;
376
+ if (visited.has(file)) continue;
377
+ visited.add(file);
378
+ let source: string;
379
+ try {
380
+ source = fs.readFileSync(file, 'utf8');
381
+ } catch {
382
+ missing.push({ from: '(entry)', spec: file, resolved: file });
383
+ continue;
384
+ }
385
+ for (const spec of parseRelativeSpecifiers(source)) {
386
+ const resolved = path.resolve(path.dirname(file), spec);
387
+ if (!fs.existsSync(resolved)) {
388
+ missing.push({ from: file, spec, resolved });
389
+ continue;
390
+ }
391
+ if (!visited.has(resolved)) stack.push(resolved);
392
+ }
393
+ }
394
+ return { visited: [...visited], missing };
395
+ }
396
+
397
+ /** Dist-manifest completeness gate — every module the entrypoint can reach exists. */
398
+ export function checkDistManifestCompleteness(entryFile: string): { ok: boolean; message: string } {
399
+ const { visited, missing } = walkEsmModuleGraph(entryFile);
400
+ if (missing.length > 0) {
401
+ const detail = missing
402
+ .slice(0, 5)
403
+ .map((m) => `${path.relative(path.dirname(entryFile), m.from)} → missing ${m.spec}`)
404
+ .join('; ');
405
+ return { ok: false, message: `dist-manifest incomplete: ${missing.length} missing module(s): ${detail}` };
406
+ }
407
+ return {
408
+ ok: true,
409
+ message: `dist-manifest complete — ${visited.length} module(s) reachable from ${path.basename(entryFile)}`,
410
+ };
411
+ }
412
+
413
+ function legHookLifecycle(ctx: Ctx): string {
414
+ const settingsPath = path.join(ctx.tmpRoot, 'home', '.claude', 'settings.json');
415
+ const entries = readInstalledClaudeHookCommands(settingsPath);
416
+ if (entries.length === 0) {
417
+ throw new Error(`no managed Claude hook commands found in ${settingsPath} (did init register hooks?)`);
418
+ }
419
+
420
+ const entryScript = fs.realpathSync(ctx.bin);
421
+ const manifestCheck = checkDistManifestCompleteness(entryScript);
422
+ if (!manifestCheck.ok) throw new Error(manifestCheck.message);
423
+
424
+ const env = { ...binEnv(ctx), CLAUDE_PROJECT_DIR: ctx.projectDir };
425
+ const fired: string[] = [];
426
+ for (const entry of entries) {
427
+ const result = invokeHookCommand(entry, { cwd: ctx.projectDir, env, projectDir: ctx.projectDir });
428
+ if (!result.ok) {
429
+ throw new Error(`hook '${entry.nativeEvent}' exited ${result.status}: ${result.stderrTail}`);
430
+ }
431
+ fired.push(`${entry.nativeEvent}=${result.status}`);
432
+ }
433
+ return `${manifestCheck.message}; fired ${fired.length} live event(s): ${fired.join(', ')}`;
434
+ }
435
+
219
436
  function legProjections(ctx: Ctx): string {
220
437
  const home = path.join(ctx.tmpRoot, 'home');
221
438
  const missing = ['.cursor/rules', '.codeium/windsurf', '.claude/skills']
@@ -278,6 +495,7 @@ const LEGS: Array<{ name: string; fn: (ctx: Ctx) => string | Promise<string> }>
278
495
  { name: 'conformance', fn: legConformance },
279
496
  { name: 'mcp-handshake', fn: legMcpHandshake },
280
497
  { name: 'hooks', fn: legHooks },
498
+ { name: 'hook-lifecycle', fn: legHookLifecycle },
281
499
  { name: 'projections', fn: legProjections },
282
500
  { name: 'uninstall', fn: legUninstall },
283
501
  { name: 'upgrade', fn: legUpgrade },
@@ -383,10 +601,31 @@ async function main(): Promise<number> {
383
601
  return failed ? 1 : 0;
384
602
  }
385
603
 
386
- main().then(
387
- (code) => process.exit(code),
388
- (err) => {
389
- log(`❌ consumer-matrix crashed: ${(err as Error).stack ?? err}`);
390
- process.exit(2);
391
- },
392
- );
604
+ // Entry guard (mirrors src/scripts/hooks/dispatch_hook.ts): a test file
605
+ // imports the pure helpers above (readInstalledClaudeHookCommands,
606
+ // checkDistManifestCompleteness, invokeHookCommand, …) without wanting a
607
+ // full `npm pack` + global-install run to fire as a side effect of the
608
+ // import. `main()` only runs when this file is the process's actual entry
609
+ // point (`npx tsx src/scripts/consumer_matrix.ts …`).
610
+ function _isCliEntry(): boolean {
611
+ if (process.argv[1] === undefined) return false;
612
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
613
+ if (import.meta.url === argvUrl) return true;
614
+ try {
615
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
616
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
617
+ return here === argv;
618
+ } catch {
619
+ return false;
620
+ }
621
+ }
622
+
623
+ if (_isCliEntry()) {
624
+ main().then(
625
+ (code) => process.exit(code),
626
+ (err) => {
627
+ log(`❌ consumer-matrix crashed: ${(err as Error).stack ?? err}`);
628
+ process.exit(2);
629
+ },
630
+ );
631
+ }
@@ -25,6 +25,7 @@ import {
25
25
  bundle_roadmap,
26
26
  } from './ai_council/bundler.js';
27
27
  import { select_chairman } from './ai_council/chairman.js';
28
+ import { jaccardSimilarity } from './_lib/text_similarity.js';
28
29
  import type { ChairmanCandidate } from './ai_council/chairman.js';
29
30
  import { synthesis_template } from './ai_council/prompts.js';
30
31
  import type {
@@ -1906,6 +1907,7 @@ function cmd_run(
1906
1907
  _isDict(ai_cfg) &&
1907
1908
  _isDict(ai_cfg['stance_tally']) &&
1908
1909
  (ai_cfg['stance_tally'] as Dict)['enabled'] === true;
1910
+ const stance_repairs: CouncilResponse[] = [];
1909
1911
  const responses = consult(members, question, budget, {
1910
1912
  table,
1911
1913
  project,
@@ -1913,6 +1915,14 @@ function cmd_run(
1913
1915
  rounds,
1914
1916
  advisor_plans,
1915
1917
  stance_tally: stance_tally_on,
1918
+ // Interactive one-line confirm (cmd_run has no --auto-continue); the
1919
+ // repaired-call cost is collected so cost_usd_actual stays honest.
1920
+ on_stance_repair: stance_tally_on
1921
+ ? (member: string): boolean => _make_repair_confirm(false)(member, 'missing stance line')
1922
+ : null,
1923
+ on_stance_repair_result: (r: CouncilResponse): void => {
1924
+ stance_repairs.push(r);
1925
+ },
1916
1926
  });
1917
1927
  const persona_labels = build_persona_labels(advisor_plans, billable);
1918
1928
  const peer_review = _maybe_run_peer_review(
@@ -1941,6 +1951,7 @@ function cmd_run(
1941
1951
  if (chairman !== null && chairman.response !== null) {
1942
1952
  all_responses.push(chairman.response);
1943
1953
  }
1954
+ all_responses.push(...stance_repairs);
1944
1955
  for (const r of all_responses) {
1945
1956
  if (r.error) {
1946
1957
  continue;
@@ -2068,6 +2079,20 @@ function _listEq(a: string[], b: string[]): boolean {
2068
2079
  return a.every((v, i) => v === b[i]);
2069
2080
  }
2070
2081
 
2082
+
2083
+ /** Phase 3 repair transport (council 2026-07-12): auto-fire under
2084
+ * --auto-continue; one-line confirm otherwise (blank/EOF stdin = N). */
2085
+ function _make_repair_confirm(auto_continue: boolean): (member: string, reason: string) => boolean {
2086
+ if (auto_continue) {
2087
+ return () => true;
2088
+ }
2089
+ return (member: string, reason: string): boolean => {
2090
+ _stdout(`\ndebate:repair ${member} — ${reason} — send one bounded repair re-prompt? [y/N]: `);
2091
+ const answer = _readStdinLine();
2092
+ return answer === 'y' || answer === 'yes';
2093
+ };
2094
+ }
2095
+
2071
2096
  function _make_debate_continue_prompt(opts: {
2072
2097
  auto_continue: boolean;
2073
2098
  stream?: ((s: string) => void) | null;
@@ -2263,6 +2288,31 @@ function cmd_debate(
2263
2288
  auto_continue: Boolean(_getattr(args, 'auto_continue', false)),
2264
2289
  });
2265
2290
 
2291
+ // Phase 3: restate pass — CLI flag OR config key; default off.
2292
+ const restate_on =
2293
+ Boolean(_getattr(args, 'restate', false)) ||
2294
+ (_isDict(ai_cfg) && _isDict(ai_cfg['restate']) && (ai_cfg['restate'] as Dict)['enabled'] === true);
2295
+ const restate_responses: CouncilResponse[] = [];
2296
+ const ask_for_divergence = (args.original_ask as string) || '';
2297
+ const on_restate = (rs: CouncilResponse[]): void => {
2298
+ restate_responses.push(...rs);
2299
+ const blocks = rs
2300
+ .filter((r) => !r.error && r.text.trim() !== '')
2301
+ .map((r) => `### ${r.provider} - ${r.model}\n\n${r.text.trim()}`);
2302
+ if (blocks.length > 0) {
2303
+ _stdout(`\n## Restatements (pre-round-1)\n\n${blocks.join('\n\n')}\n`);
2304
+ }
2305
+ // Divergence flag BEFORE further spend: a restatement far from the
2306
+ // stated ask is surfaced to the user (Jaccard over token sets).
2307
+ if (ask_for_divergence.trim() !== '') {
2308
+ for (const r of rs) {
2309
+ if (!r.error && r.text.trim() !== '' && jaccardSimilarity(ask_for_divergence, r.text) < 0.1) {
2310
+ _stderr(`⚠️ restate divergence: ${r.provider} restated the ask with little overlap — verify framing before continuing.\n`);
2311
+ }
2312
+ }
2313
+ }
2314
+ };
2315
+
2266
2316
  // Phase 3: debate gates (anti-conformity directive on round 2+). Read
2267
2317
  // defensively from the raw ai_council block — a malformed/absent
2268
2318
  // `debate_gates` reads as off, keeping the debate prompt byte-identical.
@@ -2282,6 +2332,9 @@ function cmd_debate(
2282
2332
  on_round_complete: _on_round_complete,
2283
2333
  on_continue,
2284
2334
  debate_gates: debate_gates_on,
2335
+ on_repair: debate_gates_on ? _make_repair_confirm(Boolean(_getattr(args, 'auto_continue', false))) : null,
2336
+ restate: restate_on,
2337
+ on_restate: restate_on ? on_restate : null,
2285
2338
  advisor_plans,
2286
2339
  seed_round_1: seed,
2287
2340
  });
@@ -2299,7 +2352,7 @@ function cmd_debate(
2299
2352
  }
2300
2353
 
2301
2354
  let actual_total = 0.0;
2302
- for (const rnd of all_rounds) {
2355
+ for (const rnd of [...all_rounds, restate_responses]) {
2303
2356
  for (const r of rnd) {
2304
2357
  if (r.error) {
2305
2358
  continue;
@@ -2546,6 +2599,7 @@ interface Args {
2546
2599
  single: boolean;
2547
2600
  prose_synthesis: boolean | null;
2548
2601
  auto_continue: boolean;
2602
+ restate?: boolean;
2549
2603
  continue_as_debate: string | null;
2550
2604
  // render / replay
2551
2605
  responses?: string | null;
@@ -2675,6 +2729,7 @@ function _specsFor(cmd: string): { positionals: string[]; opts: OptSpec[]; requi
2675
2729
  { flag: '--confirm', takesValue: false, apply: (o) => (o.confirm = true) },
2676
2730
  { flag: '--rounds', takesValue: true, isInt: true, apply: (o, v) => (o.rounds = v === null ? null : parseInt(v, 10)) },
2677
2731
  { flag: '--auto-continue', takesValue: false, apply: (o) => (o.auto_continue = true) },
2732
+ { flag: '--restate', takesValue: false, apply: (o) => (o.restate = true) },
2678
2733
  { flag: '--continue-as-debate', takesValue: true, apply: (o, v) => (o.continue_as_debate = v) },
2679
2734
  { flag: '--invocation', takesValue: true, choices: ['agent', 'user_explicit'], apply: (o, v) => (o.invocation = v as string) },
2680
2735
  { flag: '--proceed-anyway', takesValue: false, apply: (o) => (o.proceed_anyway = true) },