@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
@@ -58,6 +58,29 @@ export const TIERS: readonly string[] = ['public', 'vendor', 'proprietary'];
58
58
  */
59
59
  export const DEFAULT_ALLOWED_TIERS: readonly string[] = ['public'];
60
60
 
61
+ /**
62
+ * Sensitivity axis (Phase 1, road-to-feedback-8.11 / successor note to
63
+ * ADR-119) — orthogonal to `TIERS` (origin) and layered on top of the
64
+ * existing tier + redaction gate. `tier` classifies WHERE a card came from;
65
+ * `sensitivity` classifies WHETHER it may ever leave the project at all.
66
+ *
67
+ * * `prohibited` — contains redaction-class content; never leaves the
68
+ * repo. Machine-derived from the redaction scan (a card is never
69
+ * hand-labelled into this class — a positive hit forces it).
70
+ * * `project` — DEFAULT. Stays project-local; promotion is refused
71
+ * unless a human explicitly reclassifies the card to `shareable`.
72
+ * * `shareable` — eligible for the global store; must pass redaction
73
+ * AND carry full promotion provenance (see the G5 footer fields).
74
+ *
75
+ * Deliberately kept to three machine-anchored classes — a wider
76
+ * team/organization taxonomy is phantom for a single-maintainer install and
77
+ * would over-fit; revisit only once a real multi-tenant consumer exists.
78
+ */
79
+ export const SENSITIVITIES: readonly string[] = ['prohibited', 'project', 'shareable'];
80
+
81
+ /** Default sensitivity for a card with no explicit `sensitivity:` field. */
82
+ export const DEFAULT_SENSITIVITY = 'project';
83
+
61
84
  /**
62
85
  * Hard defaults applied when the setting block is absent. Default is ON for
63
86
  * the safe tiers; turning sharing on never makes promotion automatic.
@@ -384,6 +407,14 @@ const _PROVENANCE_FIELDS: readonly string[] = [
384
407
  'last_verified',
385
408
  'tier',
386
409
  'seen_in',
410
+ // G5 promotion audit-trail fields (Phase 1, road-to-feedback-8.11):
411
+ // the more consistent home for these than frontmatter, since they sit
412
+ // alongside the other promotion-time facts above, not the card's own
413
+ // origin-tier/sensitivity classification (which lives in frontmatter).
414
+ 'source_repo',
415
+ 'owner',
416
+ 'review_after',
417
+ 'promotion_reason',
387
418
  ];
388
419
 
389
420
  /** Render the global-card provenance footer (the unversioned-store audit trail). */
@@ -394,6 +425,10 @@ export function render_provenance_footer(args: {
394
425
  last_verified: string;
395
426
  tier: string;
396
427
  seen_in: string[];
428
+ source_repo: string;
429
+ owner: string;
430
+ review_after: string;
431
+ promotion_reason: string;
397
432
  }): string {
398
433
  return [
399
434
  PROVENANCE_START,
@@ -403,11 +438,20 @@ export function render_provenance_footer(args: {
403
438
  `- last_verified: ${args.last_verified}`,
404
439
  `- tier: ${args.tier}`,
405
440
  `- seen_in: ${args.seen_in.join(', ')}`,
441
+ `- source_repo: ${args.source_repo}`,
442
+ `- owner: ${args.owner}`,
443
+ `- review_after: ${args.review_after}`,
444
+ `- promotion_reason: ${_singleLine(args.promotion_reason)}`,
406
445
  PROVENANCE_END,
407
446
  '',
408
447
  ].join('\n');
409
448
  }
410
449
 
450
+ /** Collapse a human-entered reason to one line — footer fields are line-based. */
451
+ function _singleLine(s: string): string {
452
+ return s.replace(/\r\n|\r|\n/g, ' ').trim();
453
+ }
454
+
411
455
  /** Extract the provenance footer fields as a flat dict (empty if absent). */
412
456
  export function parse_provenance_footer(text: string): Record<string, string> {
413
457
  if (!text.includes(PROVENANCE_START) || !text.includes(PROVENANCE_END)) {
@@ -187,6 +187,156 @@ export function should_suggest(
187
187
  return len >= options.threshold;
188
188
  }
189
189
 
190
+ // ---------------------------------------------------------------------------
191
+ // Sensitivity gate (Phase 1, road-to-feedback-8.11 — successor note to
192
+ // ADR-119). Layered ON TOP of the existing tier/redaction gate in
193
+ // `knowledge_global_redaction.ts`: a card must clear tier + redaction FIRST,
194
+ // then clear this gate before its write is allowed to proceed.
195
+ // ---------------------------------------------------------------------------
196
+
197
+ /** Outcome of the sensitivity gate for one promotion attempt. */
198
+ export interface SensitivityGateResult {
199
+ readonly eligible: boolean;
200
+ readonly reason: string;
201
+ /** The EFFECTIVE sensitivity used for the decision (post auto-derivation). */
202
+ readonly sensitivity: string;
203
+ }
204
+
205
+ /**
206
+ * Resolve the effective sensitivity used for a promotion decision.
207
+ *
208
+ * A redaction hit always forces `prohibited`, overriding any declared value —
209
+ * this is independent of the tier gate's own `halt_on_trigger` setting, so a
210
+ * `prohibited` override never depends on that unrelated config flag. An
211
+ * unset / invalid declared value defaults to `project` (never `shareable` —
212
+ * `shareable` is never auto-assigned, only explicitly declared by a human).
213
+ */
214
+ export function resolve_effective_sensitivity(declared: string, violations_present: boolean): string {
215
+ if (violations_present) {
216
+ return 'prohibited';
217
+ }
218
+ const d = (declared ?? '').trim();
219
+ return knowledge_global.SENSITIVITIES.includes(d) ? d : knowledge_global.DEFAULT_SENSITIVITY;
220
+ }
221
+
222
+ /**
223
+ * Gate a card for promotion on the sensitivity axis.
224
+ *
225
+ * Refuses anything whose effective sensitivity is not `shareable` (including
226
+ * the machine-derived `prohibited` override), and refuses a `shareable` card
227
+ * that has no human-entered `promotion_reason` — the auto-promote
228
+ * *suggestion* only ever leads to a write once a human states why the card
229
+ * is safe to share.
230
+ */
231
+ export function gate_sensitivity_for_promotion(
232
+ declared_sensitivity: string,
233
+ options: { violations_present?: boolean; promotion_reason?: string } = {},
234
+ ): SensitivityGateResult {
235
+ const violations_present = options.violations_present ?? false;
236
+ const effective = resolve_effective_sensitivity(declared_sensitivity, violations_present);
237
+
238
+ if (effective === 'prohibited') {
239
+ return {
240
+ eligible: false,
241
+ sensitivity: effective,
242
+ reason:
243
+ 'sensitivity: prohibited — redaction-class content, never leaves the repo',
244
+ };
245
+ }
246
+ if (effective !== 'shareable') {
247
+ return {
248
+ eligible: false,
249
+ sensitivity: effective,
250
+ reason:
251
+ `sensitivity '${effective}' — promotion refused unless a human explicitly ` +
252
+ "reclassifies the card to 'shareable'",
253
+ };
254
+ }
255
+ const reason = (options.promotion_reason ?? '').trim();
256
+ if (!reason) {
257
+ return {
258
+ eligible: false,
259
+ sensitivity: effective,
260
+ reason:
261
+ "sensitivity 'shareable' but no promotion_reason given — a human must state " +
262
+ 'why this card is safe to share before it may be promoted',
263
+ };
264
+ }
265
+ return { eligible: true, sensitivity: effective, reason: 'sensitivity gate passed' };
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Revocation ledger — append-only tombstone trail (Phase 1, road-to-feedback-8.11)
270
+ // ---------------------------------------------------------------------------
271
+
272
+ export const REVOCATIONS_FILENAME = '.revocations.jsonl';
273
+
274
+ /** One append-only tombstone line: what was revoked, when, and why. */
275
+ export interface RevocationEntry {
276
+ readonly revoked_at: string;
277
+ readonly card_id: string;
278
+ readonly reason: string;
279
+ }
280
+
281
+ function _revocations_path(env: knowledge_global.EnvMapType): string {
282
+ return path.join(knowledge_global.global_store_dir(env, { create: true }), REVOCATIONS_FILENAME);
283
+ }
284
+
285
+ /**
286
+ * Append one tombstone line to the revocation ledger — the caller MUST call
287
+ * this BEFORE deleting the card/usage entry it documents. Append-only (a
288
+ * single `fs.appendFileSync`, never rewritten): no `forget` / `forget --tier`
289
+ * / `purge` call ever removes a prior tombstone, so the ledger stays a
290
+ * durable audit trail across every deletion path — including `purge`, which
291
+ * deliberately spares this one file while wiping everything else.
292
+ */
293
+ export function append_tombstone(
294
+ card_id: string,
295
+ reason: string,
296
+ options: { today?: string; env?: knowledge_global.EnvMapType } = {},
297
+ ): RevocationEntry {
298
+ const entry: RevocationEntry = {
299
+ revoked_at: options.today ?? '',
300
+ card_id,
301
+ reason: reason || 'no reason given',
302
+ };
303
+ fs.appendFileSync(_revocations_path(options.env ?? null), JSON.stringify(entry) + '\n', 'utf-8');
304
+ return entry;
305
+ }
306
+
307
+ /** Read the revocation ledger. Tolerant: missing file → []; malformed lines are skipped. */
308
+ export function load_tombstones(env: knowledge_global.EnvMapType = null): RevocationEntry[] {
309
+ let text: string;
310
+ try {
311
+ text = fs.readFileSync(
312
+ path.join(knowledge_global.global_store_dir(env), REVOCATIONS_FILENAME),
313
+ 'utf-8',
314
+ );
315
+ } catch {
316
+ return [];
317
+ }
318
+ const out: RevocationEntry[] = [];
319
+ for (const raw of text.split(/\r\n|\r|\n/)) {
320
+ const line = raw.trim();
321
+ if (!line) {
322
+ continue;
323
+ }
324
+ try {
325
+ const obj: unknown = JSON.parse(line);
326
+ if (_isPlainObject(obj) && typeof obj['card_id'] === 'string') {
327
+ out.push({
328
+ revoked_at: String(obj['revoked_at'] ?? ''),
329
+ card_id: obj['card_id'],
330
+ reason: String(obj['reason'] ?? ''),
331
+ });
332
+ }
333
+ } catch {
334
+ // corrupt line — skip; never let one bad line crash the ledger read
335
+ }
336
+ }
337
+ return out;
338
+ }
339
+
190
340
  /**
191
341
  * List cards that warrant a promotion suggestion under the current config.
192
342
  *
@@ -65,7 +65,8 @@ import {
65
65
  synthesis_template,
66
66
  system_prompt_for,
67
67
  } from './prompts.js';
68
- import { render_vote_tally, tally_stances } from './stance_tally.js';
68
+ import { count_dissenters, dissent_quota_met, is_near_duplicate } from './debate_gates.js';
69
+ import { parse_stance_line, render_vote_tally, tally_stances } from './stance_tally.js';
69
70
 
70
71
  // ── Python-format / stdlib parity helpers ────────────────────────────────
71
72
  //
@@ -419,6 +420,14 @@ export interface ConsultOptions {
419
420
  * round's prompt is byte-identical to today.
420
421
  */
421
422
  stance_tally?: boolean;
423
+ /**
424
+ * Phase 1 stance-repair transport (2026-07-12 council policy): confirm per
425
+ * member in interactive runs; `null`/absent → a missing/unparseable stance
426
+ * stays a repair-marker in the tally, never an unplanned billable call.
427
+ */
428
+ on_stance_repair?: ((member: string) => boolean) | null;
429
+ /** Receives each stance-repair response so the caller can account its cost. */
430
+ on_stance_repair_result?: ((r: CouncilResponse) => void) | null;
422
431
  }
423
432
 
424
433
  /**
@@ -515,6 +524,46 @@ export function consult(
515
524
  }
516
525
  }
517
526
 
527
+ // Phase 1 stance repair: one bounded stance-line-only re-prompt per member
528
+ // whose final reply lacks a parseable STANCE line — dispatched only through
529
+ // the on_stance_repair transport, reusing _run_round (spend gate + ledger +
530
+ // stamping unchanged). The repaired stance line is APPENDED to the member's
531
+ // final text so the deterministic tally can read it.
532
+ const on_stance_repair = opts.on_stance_repair ?? null;
533
+ if (stance_tally && on_stance_repair !== null) {
534
+ for (let i = 0; i < last_results.length; i++) {
535
+ const r = last_results[i] as CouncilResponse;
536
+ if (r.error !== null || r.text.trim() === '' || parse_stance_line(r.text) !== null) {
537
+ continue;
538
+ }
539
+ const member = members[i];
540
+ if (member === undefined || !on_stance_repair(member.name)) {
541
+ continue;
542
+ }
543
+ const repairQ = new CouncilQuestion({
544
+ mode: question.mode,
545
+ user_prompt:
546
+ `Your reply is missing the mandatory closing stance line. ` +
547
+ `Reply with ONLY that single line for your final position.\n\n${STANCE_LINE_CONTRACT}`,
548
+ max_tokens: question.max_tokens,
549
+ });
550
+ const rr = _run_round([member], repairQ, resolvedBudget, spent, {
551
+ table,
552
+ on_overrun,
553
+ project,
554
+ original_ask,
555
+ advisor_plans,
556
+ });
557
+ const rep = rr[0];
558
+ if (rep !== undefined) {
559
+ opts.on_stance_repair_result?.(rep);
560
+ if (rep.error === null && parse_stance_line(rep.text) !== null) {
561
+ r.text = `${r.text.trim()}\n\n${rep.text.trim()}`;
562
+ }
563
+ }
564
+ }
565
+ }
566
+
518
567
  return last_results;
519
568
  }
520
569
 
@@ -895,6 +944,90 @@ function _augment_for_debate_round(
895
944
  );
896
945
  }
897
946
 
947
+ /**
948
+ * Phase 3 post-round enforcement (2026-07-12 council policy): deterministic
949
+ * detectors flag members whose round added nothing (novelty near-duplicate of
950
+ * their own prior round) or when the round collapsed toward agreement (dissent
951
+ * quota unmet → the most-recently-converged member). At most ONE bounded repair
952
+ * re-prompt per member per round, dispatched only through the `on_repair`
953
+ * transport (confirm-interactive / auto-fire under --auto-continue); the repair
954
+ * call reuses `_run_round`, so the projected-spend gate, ledger, and metadata
955
+ * stamping apply unchanged. A successful repair REPLACES the member's entry so
956
+ * downstream rounds/persistence see the repaired reply.
957
+ */
958
+ function _run_debate_gate_repairs(
959
+ members: ExternalAIClient[],
960
+ results: CouncilResponse[],
961
+ prior: CouncilResponse[],
962
+ round_question: CouncilQuestion,
963
+ budget: CostBudget,
964
+ spent: Spent,
965
+ round_opts: {
966
+ table: PriceTable | null;
967
+ on_overrun: OnOverrunCallback | null;
968
+ project: ProjectContext | null;
969
+ original_ask: string;
970
+ advisor_plans: Map<string, AdvisorPlan> | null;
971
+ },
972
+ on_repair: ((member: string, reason: string) => boolean) | null,
973
+ ): CouncilResponse[] {
974
+ const flagged: Array<{ idx: number; reason: string }> = [];
975
+ const okText = (r: CouncilResponse | undefined): string =>
976
+ r !== undefined && r.error === null ? r.text : '';
977
+ for (let i = 0; i < results.length; i++) {
978
+ const curr = okText(results[i]);
979
+ const prev = okText(prior[i]);
980
+ if (curr !== '' && prev !== '' && is_near_duplicate(prev, curr)) {
981
+ flagged.push({ idx: i, reason: 'novelty: near-duplicate of your own prior round' });
982
+ }
983
+ }
984
+ const texts = results.map((r) => okText(r));
985
+ if (!dissent_quota_met(texts.filter((t) => t !== ''))) {
986
+ // Most-recently-converged heuristic: the LAST member with a real reply
987
+ // that carries no objection marker.
988
+ for (let i = results.length - 1; i >= 0; i--) {
989
+ const t = okText(results[i]);
990
+ if (t !== '' && count_dissenters([t]) === 0 && !flagged.some((f) => f.idx === i)) {
991
+ flagged.push({ idx: i, reason: 'dissent quota unmet: state a genuine objection or confirm with a named reason' });
992
+ break;
993
+ }
994
+ }
995
+ }
996
+ if (flagged.length === 0 || on_repair === null) {
997
+ return results;
998
+ }
999
+ const repaired = new Set<number>();
1000
+ const out = [...results];
1001
+ for (const f of flagged) {
1002
+ if (repaired.has(f.idx)) {
1003
+ continue; // hard cap: <= 1 repair per member per round
1004
+ }
1005
+ const member = members[f.idx];
1006
+ if (member === undefined) {
1007
+ continue;
1008
+ }
1009
+ if (!on_repair(member.name, f.reason)) {
1010
+ continue;
1011
+ }
1012
+ repaired.add(f.idx);
1013
+ const repairQ = new CouncilQuestion({
1014
+ mode: round_question.mode,
1015
+ user_prompt:
1016
+ `${round_question.user_prompt}\n\n---\n\n` +
1017
+ `REPAIR RE-PROMPT (${f.reason}). Your previous reply this round did not ` +
1018
+ `advance the debate. Respond again: either a genuinely updated position ` +
1019
+ `naming the specific flaw that moved you, or an explicit defense with NEW evidence.`,
1020
+ max_tokens: round_question.max_tokens,
1021
+ });
1022
+ const rr = _run_round([member], repairQ, budget, spent, round_opts);
1023
+ const r0 = rr[0];
1024
+ if (r0 !== undefined && r0.error === null && r0.text.trim() !== '') {
1025
+ out[f.idx] = r0;
1026
+ }
1027
+ }
1028
+ return out;
1029
+ }
1030
+
898
1031
  export interface RunDebateOptions {
899
1032
  budget?: CostBudget | null;
900
1033
  table?: PriceTable | null;
@@ -914,6 +1047,22 @@ export interface RunDebateOptions {
914
1047
  * prompt is byte-identical to today.
915
1048
  */
916
1049
  debate_gates?: boolean;
1050
+ /**
1051
+ * Phase 3 repair transport, per the 2026-07-12 council policy
1052
+ * (`debate_gates.repair_action`): the CLI passes `() => true` under
1053
+ * `--auto-continue` (auto-fire under the cap) or an interactive one-line
1054
+ * confirm otherwise. `null`/absent → gates detect but never dispatch
1055
+ * (no unplanned spend without an explicit transport).
1056
+ */
1057
+ on_repair?: ((member: string, reason: string) => boolean) | null;
1058
+ /**
1059
+ * Phase 3 restate pass (`ai_council.restate.enabled` / `--restate`): a
1060
+ * pre-round-1 billable call per member collecting a <=50-word restatement
1061
+ * + one alternative framing. Default off -> no extra calls, byte-identical.
1062
+ */
1063
+ restate?: boolean;
1064
+ /** Receives the restate responses (rendering, divergence flags, cost). */
1065
+ on_restate?: ((responses: CouncilResponse[]) => void) | null;
917
1066
  }
918
1067
 
919
1068
  /**
@@ -975,6 +1124,27 @@ export function run_debate(
975
1124
  const all_rounds: CouncilResponse[][] = [];
976
1125
  let current_user_prompt = question.user_prompt;
977
1126
 
1127
+ // Phase 3 restate: one pre-round-1 call per member. Runs BEFORE any debate
1128
+ // spend so a diverging restatement can be flagged before round-2 cost.
1129
+ if (opts.restate === true) {
1130
+ const restateQ = new CouncilQuestion({
1131
+ mode: question.mode,
1132
+ user_prompt:
1133
+ `Before the debate begins: RESTATE the question below in at most 50 words, ` +
1134
+ `then offer ONE alternative framing of it. Do not argue a position yet.\n\n---\n\n` +
1135
+ `${question.user_prompt}`,
1136
+ max_tokens: question.max_tokens,
1137
+ });
1138
+ const restated = _run_round(members, restateQ, budget, spent, {
1139
+ table,
1140
+ on_overrun,
1141
+ project,
1142
+ original_ask,
1143
+ advisor_plans,
1144
+ });
1145
+ opts.on_restate?.(restated);
1146
+ }
1147
+
978
1148
  for (let round_idx = 0; round_idx < max_rounds; round_idx++) {
979
1149
  const round_number = round_idx + 1;
980
1150
  let results: CouncilResponse[];
@@ -1000,6 +1170,23 @@ export function run_debate(
1000
1170
  });
1001
1171
  }
1002
1172
 
1173
+ if (opts.debate_gates === true && round_idx > 0 && all_rounds.length > 0) {
1174
+ results = _run_debate_gate_repairs(
1175
+ members,
1176
+ results,
1177
+ all_rounds[all_rounds.length - 1] as CouncilResponse[],
1178
+ // round_idx > 0 here, so current_user_prompt IS this round's prompt.
1179
+ new CouncilQuestion({
1180
+ mode: question.mode,
1181
+ user_prompt: current_user_prompt,
1182
+ max_tokens: question.max_tokens,
1183
+ }),
1184
+ budget,
1185
+ spent,
1186
+ { table, on_overrun, project, original_ask, advisor_plans },
1187
+ opts.on_repair ?? null,
1188
+ );
1189
+ }
1003
1190
  all_rounds.push(results);
1004
1191
  if (on_round_complete !== null) {
1005
1192
  on_round_complete(round_number, results);