@zelari/core 0.7.12 → 0.7.14

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 (138) hide show
  1. package/dist/agents/councilApi.d.ts +135 -0
  2. package/dist/agents/councilApi.d.ts.map +1 -1
  3. package/dist/agents/councilApi.js +520 -7
  4. package/dist/agents/councilApi.js.map +1 -1
  5. package/dist/agents/roles.d.ts.map +1 -1
  6. package/dist/agents/roles.js +11 -3
  7. package/dist/agents/roles.js.map +1 -1
  8. package/dist/agents/toolSchemas.d.ts.map +1 -1
  9. package/dist/agents/toolSchemas.js +32 -3
  10. package/dist/agents/toolSchemas.js.map +1 -1
  11. package/dist/core/AgentHarness.d.ts +21 -0
  12. package/dist/core/AgentHarness.d.ts.map +1 -1
  13. package/dist/core/AgentHarness.js +153 -0
  14. package/dist/core/AgentHarness.js.map +1 -1
  15. package/dist/core/tools/builtin/filesystem.d.ts.map +1 -1
  16. package/dist/core/tools/builtin/filesystem.js +4 -2
  17. package/dist/core/tools/builtin/filesystem.js.map +1 -1
  18. package/dist/council/completion/buildCompletion.d.ts +23 -0
  19. package/dist/council/completion/buildCompletion.d.ts.map +1 -0
  20. package/dist/council/completion/buildCompletion.js +75 -0
  21. package/dist/council/completion/buildCompletion.js.map +1 -0
  22. package/dist/council/completion/index.d.ts +3 -0
  23. package/dist/council/completion/index.d.ts.map +1 -0
  24. package/dist/council/completion/index.js +3 -0
  25. package/dist/council/completion/index.js.map +1 -0
  26. package/dist/council/completion/types.d.ts +38 -0
  27. package/dist/council/completion/types.d.ts.map +1 -0
  28. package/dist/council/completion/types.js +2 -0
  29. package/dist/council/completion/types.js.map +1 -0
  30. package/dist/council/index.d.ts +4 -0
  31. package/dist/council/index.d.ts.map +1 -1
  32. package/dist/council/index.js +4 -0
  33. package/dist/council/index.js.map +1 -1
  34. package/dist/council/lessons/index.d.ts +7 -0
  35. package/dist/council/lessons/index.d.ts.map +1 -0
  36. package/dist/council/lessons/index.js +7 -0
  37. package/dist/council/lessons/index.js.map +1 -0
  38. package/dist/council/lessons/io.d.ts +5 -0
  39. package/dist/council/lessons/io.d.ts.map +1 -0
  40. package/dist/council/lessons/io.js +30 -0
  41. package/dist/council/lessons/io.js.map +1 -0
  42. package/dist/council/lessons/isAnswerLeak.d.ts +7 -0
  43. package/dist/council/lessons/isAnswerLeak.d.ts.map +1 -0
  44. package/dist/council/lessons/isAnswerLeak.js +26 -0
  45. package/dist/council/lessons/isAnswerLeak.js.map +1 -0
  46. package/dist/council/lessons/recallLessons.d.ts +8 -0
  47. package/dist/council/lessons/recallLessons.d.ts.map +1 -0
  48. package/dist/council/lessons/recallLessons.js +56 -0
  49. package/dist/council/lessons/recallLessons.js.map +1 -0
  50. package/dist/council/lessons/recordFailure.d.ts +7 -0
  51. package/dist/council/lessons/recordFailure.d.ts.map +1 -0
  52. package/dist/council/lessons/recordFailure.js +86 -0
  53. package/dist/council/lessons/recordFailure.js.map +1 -0
  54. package/dist/council/lessons/signatures.d.ts +6 -0
  55. package/dist/council/lessons/signatures.d.ts.map +1 -0
  56. package/dist/council/lessons/signatures.js +31 -0
  57. package/dist/council/lessons/signatures.js.map +1 -0
  58. package/dist/council/lessons/types.d.ts +27 -0
  59. package/dist/council/lessons/types.d.ts.map +1 -0
  60. package/dist/council/lessons/types.js +2 -0
  61. package/dist/council/lessons/types.js.map +1 -0
  62. package/dist/council/modeBanners.d.ts +24 -1
  63. package/dist/council/modeBanners.d.ts.map +1 -1
  64. package/dist/council/modeBanners.js +31 -4
  65. package/dist/council/modeBanners.js.map +1 -1
  66. package/dist/council/scope/extractTaskScope.d.ts +35 -0
  67. package/dist/council/scope/extractTaskScope.d.ts.map +1 -0
  68. package/dist/council/scope/extractTaskScope.js +174 -0
  69. package/dist/council/scope/extractTaskScope.js.map +1 -0
  70. package/dist/council/scope/index.d.ts +3 -0
  71. package/dist/council/scope/index.d.ts.map +1 -0
  72. package/dist/council/scope/index.js +3 -0
  73. package/dist/council/scope/index.js.map +1 -0
  74. package/dist/council/scope/nfrSpecWarn.d.ts +6 -0
  75. package/dist/council/scope/nfrSpecWarn.d.ts.map +1 -0
  76. package/dist/council/scope/nfrSpecWarn.js +21 -0
  77. package/dist/council/scope/nfrSpecWarn.js.map +1 -0
  78. package/dist/council/verification/autofix.d.ts +12 -0
  79. package/dist/council/verification/autofix.d.ts.map +1 -0
  80. package/dist/council/verification/autofix.js +41 -0
  81. package/dist/council/verification/autofix.js.map +1 -0
  82. package/dist/council/verification/citeVerify.d.ts +13 -0
  83. package/dist/council/verification/citeVerify.d.ts.map +1 -0
  84. package/dist/council/verification/citeVerify.js +73 -0
  85. package/dist/council/verification/citeVerify.js.map +1 -0
  86. package/dist/council/verification/completion.d.ts +19 -0
  87. package/dist/council/verification/completion.d.ts.map +1 -0
  88. package/dist/council/verification/completion.js +50 -0
  89. package/dist/council/verification/completion.js.map +1 -0
  90. package/dist/council/verification/degraded.d.ts +26 -0
  91. package/dist/council/verification/degraded.d.ts.map +1 -0
  92. package/dist/council/verification/degraded.js +35 -0
  93. package/dist/council/verification/degraded.js.map +1 -0
  94. package/dist/council/verification/honesty.d.ts +6 -0
  95. package/dist/council/verification/honesty.d.ts.map +1 -0
  96. package/dist/council/verification/honesty.js +28 -0
  97. package/dist/council/verification/honesty.js.map +1 -0
  98. package/dist/council/verification/implementationDelivery.d.ts +26 -0
  99. package/dist/council/verification/implementationDelivery.d.ts.map +1 -0
  100. package/dist/council/verification/implementationDelivery.js +70 -0
  101. package/dist/council/verification/implementationDelivery.js.map +1 -0
  102. package/dist/council/verification/index.d.ts +15 -0
  103. package/dist/council/verification/index.d.ts.map +1 -0
  104. package/dist/council/verification/index.js +15 -0
  105. package/dist/council/verification/index.js.map +1 -0
  106. package/dist/council/verification/inlineJsAutofix.d.ts +8 -0
  107. package/dist/council/verification/inlineJsAutofix.d.ts.map +1 -0
  108. package/dist/council/verification/inlineJsAutofix.js +85 -0
  109. package/dist/council/verification/inlineJsAutofix.js.map +1 -0
  110. package/dist/council/verification/microGate.d.ts +18 -0
  111. package/dist/council/verification/microGate.d.ts.map +1 -0
  112. package/dist/council/verification/microGate.js +81 -0
  113. package/dist/council/verification/microGate.js.map +1 -0
  114. package/dist/council/verification/motionAutofix.d.ts +8 -0
  115. package/dist/council/verification/motionAutofix.d.ts.map +1 -0
  116. package/dist/council/verification/motionAutofix.js +160 -0
  117. package/dist/council/verification/motionAutofix.js.map +1 -0
  118. package/dist/council/verification/parseCssMotion.d.ts +22 -0
  119. package/dist/council/verification/parseCssMotion.d.ts.map +1 -0
  120. package/dist/council/verification/parseCssMotion.js +142 -0
  121. package/dist/council/verification/parseCssMotion.js.map +1 -0
  122. package/dist/council/verification/runChecks.d.ts +10 -0
  123. package/dist/council/verification/runChecks.d.ts.map +1 -0
  124. package/dist/council/verification/runChecks.js +254 -0
  125. package/dist/council/verification/runChecks.js.map +1 -0
  126. package/dist/council/verification/synthesisAudit.d.ts +14 -0
  127. package/dist/council/verification/synthesisAudit.d.ts.map +1 -0
  128. package/dist/council/verification/synthesisAudit.js +106 -0
  129. package/dist/council/verification/synthesisAudit.js.map +1 -0
  130. package/dist/council/verification/tiers.d.ts +8 -0
  131. package/dist/council/verification/tiers.d.ts.map +1 -0
  132. package/dist/council/verification/tiers.js +46 -0
  133. package/dist/council/verification/tiers.js.map +1 -0
  134. package/dist/council/verification/types.d.ts +53 -0
  135. package/dist/council/verification/types.d.ts.map +1 -0
  136. package/dist/council/verification/types.js +5 -0
  137. package/dist/council/verification/types.js.map +1 -0
  138. package/package.json +1 -1
@@ -1,11 +1,19 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { getCouncilAgents, swapMembers } from './roles.js';
2
4
  import { getProviderTools } from './toolSchemas.js';
3
5
  import { buildSystemPrompt, computeAgentTools } from './systemPromptBuilder.js';
4
6
  import { getAllTools } from './tools.js';
5
7
  import { createBrainEvent } from '../shared/events.js';
6
- import { AgentHarness } from '../core/AgentHarness.js';
8
+ import { AgentHarness, normalizeTextToolArgs, parseTextToolCalls, } from '../core/AgentHarness.js';
7
9
  import { councilModeBanner } from '../council/modeBanners.js';
8
10
  import { councilTierFromSize } from '../council/runMode.js';
11
+ import { parseProjectRootFromWorkspaceContext, runChairmanMicroGate, } from '../council/verification/microGate.js';
12
+ import { buildImplementationVerifyRetryPrompt, checkImplementationCompletion, resolveVerifyRetryTool, } from '../council/verification/completion.js';
13
+ import { warnIfNfrSpecMissing } from '../council/scope/nfrSpecWarn.js';
14
+ import { loadNfrSpec, DEFAULT_NFR_SPEC, runImplementationVerification, } from '../council/verification/runChecks.js';
15
+ import { buildDeliveryFixPrompt, buildImplementationWriteRetryPrompt, checkImplementationDelivery, countEmittedWriteTools, filterDeliveryBlockingFails, } from '../council/verification/implementationDelivery.js';
16
+ import { applyInlineJsAutofix } from '../council/verification/inlineJsAutofix.js';
9
17
  /**
10
18
  * Council members whose tool-emission retry is DISABLED.
11
19
  *
@@ -60,6 +68,24 @@ export function cleanAgentContent(text) {
60
68
  .replace(/---QUESTION---[\s\S]*?---END---/g, '')
61
69
  .trim();
62
70
  }
71
+ /**
72
+ * Tools that mutate project files. In implementation-mode council runs only the
73
+ * chairman (Lucifero) implements; specialists + Minosse analyze and hand off.
74
+ * We strip these from every non-implementer so multiple agents never edit the
75
+ * same files (multi-writer chaos) and "who implemented" stays unambiguous.
76
+ * Note: specialists inherit write tools via skill `requiredTools`, so this must
77
+ * filter the RESULT of computeAgentTools, not just the declared role tools.
78
+ */
79
+ export const MUTATING_PROJECT_TOOLS = ['write_file', 'edit_file'];
80
+ /**
81
+ * Remove file-mutating tools for non-implementer members in implementation mode.
82
+ * Design-phase and the implementer (chairman) keep the full set unchanged.
83
+ */
84
+ export function restrictImplementationWrites(toolNames, opts) {
85
+ if (opts.runMode !== 'implementation' || opts.isImplementer)
86
+ return toolNames;
87
+ return toolNames.filter((t) => !MUTATING_PROJECT_TOOLS.includes(t));
88
+ }
63
89
  function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, priorOutputs, aiConfig, executableTools, runMode = 'implementation') {
64
90
  // v0.7.5: the AVAILABLE TOOLS prompt block must match the schemas the
65
91
  // harness actually advertises. The v0.7.3 fix filtered the schemas
@@ -80,7 +106,7 @@ function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, pr
80
106
  });
81
107
  const messages = [
82
108
  { role: 'system', content: enhancedSystemPrompt },
83
- { role: 'system', content: councilModeBanner(runMode) },
109
+ { role: 'system', content: councilModeBanner(runMode, { isImplementer: agent.id === 'lucifer' }) },
84
110
  { role: 'system', content: 'IMPORTANT: Before making any tool calls or expensive operations, check if the information already exists in the shared context from previous agents. Avoid redundant work.' },
85
111
  ];
86
112
  if (ragContext) {
@@ -202,7 +228,13 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
202
228
  const override = config.agentModels?.[agent.id];
203
229
  const effectiveProvider = override?.providerId ?? config.provider ?? 'minimax';
204
230
  const effectiveModel = override?.model ?? config.model;
205
- const agentToolNames = filterExecutable(computeAgentTools(agent, config.aiConfig));
231
+ // Specialists analyze and hand off; only the chairman (Lucifero) implements
232
+ // in implementation mode. Strip write/edit so members don't all edit the
233
+ // same files. (Design-phase and the chairman keep the full set.)
234
+ const agentToolNames = filterExecutable(restrictImplementationWrites(computeAgentTools(agent, config.aiConfig), {
235
+ runMode,
236
+ isImplementer: false,
237
+ }));
206
238
  const agentTools = agentToolNames.length > 0
207
239
  ? getProviderTools(agentToolNames).map((t) => ({
208
240
  name: t.function.name,
@@ -300,6 +332,9 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
300
332
  else if (isDesignPhase) {
301
333
  enforceDesignPhaseToolEmissions(agent.id, emittedToolNames);
302
334
  }
335
+ if (isDesignPhase) {
336
+ warnIfNfrSpecMissing(agent.id, userMessage, emittedToolNames);
337
+ }
303
338
  const memberDuration = Date.now() - memberStart;
304
339
  emitMemberCost({
305
340
  memberId: agent.id,
@@ -369,11 +404,11 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
369
404
  sessionId,
370
405
  messages: buildAgentMessages(oracle, `Review these proposals for: "${userMessage}"`, '', '', anonymized, config.aiConfig, executableNames, runMode),
371
406
  tools: (() => {
372
- const oracleToolNames = filterExecutable(Array.from(new Set([
407
+ const oracleToolNames = filterExecutable(restrictImplementationWrites(Array.from(new Set([
373
408
  'createDocument',
374
409
  'searchDocuments',
375
410
  ...computeAgentTools(oracle, config.aiConfig),
376
- ])));
411
+ ])), { runMode, isImplementer: false }));
377
412
  return oracleToolNames.length > 0
378
413
  ? getProviderTools(oracleToolNames).map((tool) => ({
379
414
  name: tool.function.name,
@@ -550,6 +585,14 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
550
585
  // check below (Lucifero MUST emit at least one createDocument for
551
586
  // the synthesis; see enforceDesignPhaseToolEmissions).
552
587
  const emittedToolNames = [];
588
+ const pendingChairmanWrites = new Map();
589
+ let successfulWriteCount = 0;
590
+ // Increment 3: accumulate deduped motion violations across the whole turn
591
+ // (keyed by id|file|line) and remember the target files written, so we can
592
+ // run ONE post-turn fix pass instead of emitting a repeated per-write flood.
593
+ const chairmanViolations = new Map();
594
+ const changedTargetFiles = new Set();
595
+ let chairmanProjectRoot = parseProjectRootFromWorkspaceContext(config.workspaceContext ?? '');
553
596
  const memberStart = Date.now();
554
597
  try {
555
598
  for await (const event of chairmanHarness.run()) {
@@ -557,6 +600,47 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
557
600
  if (event.type === 'tool_execution_start') {
558
601
  toolCalls += 1;
559
602
  emittedToolNames.push(event.toolName);
603
+ if (!isDesignPhase &&
604
+ (event.toolName === 'write_file' || event.toolName === 'edit_file') &&
605
+ typeof event.args.path === 'string') {
606
+ pendingChairmanWrites.set(event.toolCallId, {
607
+ path: event.args.path,
608
+ toolName: event.toolName,
609
+ });
610
+ }
611
+ }
612
+ if (!isDesignPhase && event.type === 'tool_execution_end') {
613
+ const pending = pendingChairmanWrites.get(event.toolCallId);
614
+ if (pending) {
615
+ pendingChairmanWrites.delete(event.toolCallId);
616
+ if (!event.isError) {
617
+ let wrote = pending.toolName === 'write_file';
618
+ if (pending.toolName === 'edit_file' && typeof event.result === 'string') {
619
+ try {
620
+ const parsed = JSON.parse(event.result);
621
+ wrote = (parsed.occurrencesReplaced ?? 0) > 0;
622
+ }
623
+ catch {
624
+ wrote = false;
625
+ }
626
+ }
627
+ if (wrote) {
628
+ successfulWriteCount += 1;
629
+ const projectRoot = parseProjectRootFromWorkspaceContext(config.workspaceContext);
630
+ if (projectRoot) {
631
+ chairmanProjectRoot = projectRoot;
632
+ changedTargetFiles.add(pending.path);
633
+ for (const w of runChairmanMicroGate({
634
+ projectRoot,
635
+ relPath: pending.path,
636
+ zelariRoot: `${projectRoot}/.zelari`,
637
+ })) {
638
+ chairmanViolations.set(`${w.id}|${w.file}|${w.line ?? ''}`, w);
639
+ }
640
+ }
641
+ }
642
+ }
643
+ }
560
644
  }
561
645
  if (event.type === 'message_end' && event.usage) {
562
646
  usage = event.usage;
@@ -572,7 +656,8 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
572
656
  // We must detect this and mark the chairman as errored so the
573
657
  // member_cost reflects reality, otherwise the synthesis appears
574
658
  // successful when in fact the model never produced text.
575
- if (event.severity !== 'cancelled') {
659
+ // text_tools_parse_failed is advisory — the post-turn fix loop still runs.
660
+ if (event.severity !== 'cancelled' && event.code !== 'text_tools_parse_failed') {
576
661
  errored = true;
577
662
  lastErrorMessage = event.message;
578
663
  }
@@ -616,6 +701,99 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
616
701
  else if (isDesignPhase) {
617
702
  enforceDesignPhaseToolEmissions(chairman.id, emittedToolNames);
618
703
  }
704
+ else if (!errored) {
705
+ // Increment 5: implementation delivery — force writes when prose-only,
706
+ // replay ---TOOLS---, motion fix, then verify-driven delivery loop.
707
+ if (chairmanProjectRoot) {
708
+ const zelariRoot = `${chairmanProjectRoot}/.zelari`;
709
+ const spec = loadNfrSpec(zelariRoot) ?? DEFAULT_NFR_SPEC;
710
+ for (const rel of spec.targets) {
711
+ if (!existsSync(join(chairmanProjectRoot, rel)))
712
+ continue;
713
+ changedTargetFiles.add(rel);
714
+ for (const w of runChairmanMicroGate({ projectRoot: chairmanProjectRoot, relPath: rel, zelariRoot })) {
715
+ chairmanViolations.set(`${w.id}|${w.file}|${w.line ?? ''}`, w);
716
+ }
717
+ }
718
+ }
719
+ if (fullText.includes('---TOOLS---') &&
720
+ chairmanProjectRoot &&
721
+ config.tools) {
722
+ const replayed = yield* replayChairmanTextTools({
723
+ synthesisText: fullText,
724
+ projectRoot: chairmanProjectRoot,
725
+ toolRegistry: config.tools,
726
+ sessionId,
727
+ memberId: chairman.id,
728
+ });
729
+ successfulWriteCount += replayed;
730
+ const zelariRootReplay = `${chairmanProjectRoot}/.zelari`;
731
+ const specReplay = loadNfrSpec(zelariRootReplay) ?? DEFAULT_NFR_SPEC;
732
+ for (const rel of specReplay.targets) {
733
+ if (!existsSync(join(chairmanProjectRoot, rel)))
734
+ continue;
735
+ changedTargetFiles.add(rel);
736
+ for (const w of runChairmanMicroGate({
737
+ projectRoot: chairmanProjectRoot,
738
+ relPath: rel,
739
+ zelariRoot: zelariRootReplay,
740
+ })) {
741
+ chairmanViolations.set(`${w.id}|${w.file}|${w.line ?? ''}`, w);
742
+ }
743
+ }
744
+ }
745
+ if (chairmanProjectRoot) {
746
+ const deliveryCheck = checkImplementationDelivery(successfulWriteCount, countEmittedWriteTools(emittedToolNames));
747
+ if (!deliveryCheck.ok) {
748
+ yield* applyImplementationWriteRetry({
749
+ chairman,
750
+ check: deliveryCheck,
751
+ sessionId,
752
+ userMessage,
753
+ agentOutputs,
754
+ config,
755
+ effectiveProvider,
756
+ effectiveModel,
757
+ executableNames,
758
+ onToolCall: () => { toolCalls += 1; },
759
+ onSuccessfulWrite: () => { successfulWriteCount += 1; },
760
+ onCouncilStatus: callbacks.onCouncilStatus,
761
+ });
762
+ }
763
+ }
764
+ if (chairmanViolations.size > 0 && chairmanProjectRoot) {
765
+ yield* runChairmanFixLoop({
766
+ chairman,
767
+ violations: chairmanViolations,
768
+ changedFiles: changedTargetFiles,
769
+ projectRoot: chairmanProjectRoot,
770
+ executableNames,
771
+ sessionId,
772
+ userMessage,
773
+ agentOutputs,
774
+ config,
775
+ effectiveProvider,
776
+ effectiveModel,
777
+ onToolCall: () => { toolCalls += 1; },
778
+ });
779
+ }
780
+ if (chairmanProjectRoot) {
781
+ yield* runChairmanDeliveryLoop({
782
+ chairman,
783
+ projectRoot: chairmanProjectRoot,
784
+ changedFiles: changedTargetFiles,
785
+ executableNames,
786
+ sessionId,
787
+ userMessage,
788
+ agentOutputs,
789
+ config,
790
+ effectiveProvider,
791
+ effectiveModel,
792
+ onToolCall: () => { toolCalls += 1; },
793
+ onCouncilStatus: callbacks.onCouncilStatus,
794
+ });
795
+ }
796
+ }
619
797
  const memberDuration = Date.now() - memberStart;
620
798
  // If the chairman errored mid-flight but produced some text, keep it
621
799
  // (don't lose partial synthesis) but mark the run as errored.
@@ -653,6 +831,338 @@ export async function* runCouncilPure(userMessage, config, callbacks = {}) {
653
831
  durationMs: 0,
654
832
  };
655
833
  }
834
+ /**
835
+ * Implementation-mode anti-resa retry: force grep/bash after writes.
836
+ */
837
+ export async function* applyCompletionRetry(args) {
838
+ const check = checkImplementationCompletion(args.emittedToolNames);
839
+ if (check.ok)
840
+ return;
841
+ const retryTool = resolveVerifyRetryTool(args.executableNames);
842
+ if (!retryTool) {
843
+ // eslint-disable-next-line no-console
844
+ console.warn('[council] implementation verify retry skipped — no grep_content/bash in registry');
845
+ return;
846
+ }
847
+ if (!shouldRetryMember([retryTool], 0))
848
+ return;
849
+ // eslint-disable-next-line no-console
850
+ console.warn(`[council] ${args.agent.id} retrying missing verify tool: ${retryTool}`);
851
+ try {
852
+ const retryGenerator = runRetryTurnForMember({
853
+ agent: args.agent,
854
+ missingToolNames: [retryTool],
855
+ executableTools: args.executableNames,
856
+ userMessage: args.userMessage,
857
+ ragContext: args.config.ragContext,
858
+ workspaceContext: args.config.workspaceContext,
859
+ priorOutputs: args.agentOutputs,
860
+ aiConfig: args.config.aiConfig,
861
+ sessionId: args.sessionId,
862
+ effectiveModel: args.effectiveModel,
863
+ effectiveProvider: args.effectiveProvider,
864
+ eventBus: args.config.eventBus,
865
+ toolRegistry: args.config.tools,
866
+ providerStream: args.config.providerStream,
867
+ runMode: args.config.runMode,
868
+ retryPrompt: buildImplementationVerifyRetryPrompt(retryTool),
869
+ });
870
+ for await (const event of retryGenerator) {
871
+ if (event.type === 'tool_execution_start') {
872
+ args.onToolCall();
873
+ args.emittedToolNames.push(event.toolName);
874
+ }
875
+ yield event;
876
+ }
877
+ }
878
+ catch (retryErr) {
879
+ // eslint-disable-next-line no-console
880
+ console.error(`[council] ${args.agent.id} verify retry failed:`, retryErr);
881
+ }
882
+ const after = checkImplementationCompletion(args.emittedToolNames);
883
+ if (!after.ok) {
884
+ // eslint-disable-next-line no-console
885
+ console.warn(`[council] ${args.agent.id} still missing verify after retry: ${after.reason}`);
886
+ }
887
+ }
888
+ /**
889
+ * Build the scoped fix instruction for the chairman from deterministic motion
890
+ * violations. Groups by file, lists file:line + rule, and constrains the model
891
+ * to fix ONLY these (no new features, no rewrites). Exported for testing.
892
+ */
893
+ export function buildMotionFixPrompt(violations) {
894
+ const byFile = new Map();
895
+ for (const v of violations) {
896
+ const file = v.file || 'index.html';
897
+ const loc = v.line ? `${file}:L${v.line}` : file;
898
+ const list = byFile.get(file) ?? [];
899
+ list.push(` - ${loc}: ${v.message}`);
900
+ byFile.set(file, list);
901
+ }
902
+ const blocks = Array.from(byFile.values()).map((lines) => lines.join('\n')).join('\n');
903
+ return (`Deterministic verification found ${violations.length} motion violation(s) in the file(s) you just edited. ` +
904
+ `Fix ONLY these — do not add features, do not rewrite sections, do not touch anything else:\n${blocks}\n\n` +
905
+ `Rules: animate ONLY transform and opacity. Replace any box-shadow / background / background-position / ` +
906
+ `filter / color / border-color / width / height / grid-template-rows used in @keyframes or transitions with ` +
907
+ `transform/opacity equivalents (e.g. render a glow via a pseudo-element that scales and fades). For every ` +
908
+ `classList.add('x') in the script, add a matching '.x' CSS rule. Use read_file to see the exact lines, then ` +
909
+ `edit_file. When the listed items are fixed, stop — no summary.`);
910
+ }
911
+ /**
912
+ * Forced retry when Lucifero finished without a successful write_file/edit_file.
913
+ */
914
+ export async function* applyImplementationWriteRetry(args) {
915
+ if (args.check.ok)
916
+ return;
917
+ if (!shouldRetryMember(['write_file'], 0))
918
+ return;
919
+ const statusMsg = `[council] ${args.chairman.id} implementation write retry: ${args.check.missing.join(', ')}`;
920
+ args.onCouncilStatus?.(statusMsg);
921
+ // eslint-disable-next-line no-console
922
+ console.warn(statusMsg);
923
+ try {
924
+ const retryGenerator = runRetryTurnForMember({
925
+ agent: args.chairman,
926
+ missingToolNames: ['read_file', 'write_file', 'edit_file'],
927
+ minPerTool: { read_file: 3, edit_file: 8, write_file: 1 },
928
+ executableTools: args.executableNames,
929
+ userMessage: args.userMessage,
930
+ ragContext: args.config.ragContext,
931
+ workspaceContext: args.config.workspaceContext,
932
+ priorOutputs: args.agentOutputs,
933
+ aiConfig: args.config.aiConfig,
934
+ sessionId: args.sessionId,
935
+ effectiveModel: args.effectiveModel,
936
+ effectiveProvider: args.effectiveProvider,
937
+ eventBus: args.config.eventBus,
938
+ toolRegistry: args.config.tools,
939
+ providerStream: args.config.providerStream,
940
+ runMode: 'implementation',
941
+ retryPrompt: buildImplementationWriteRetryPrompt(args.userMessage),
942
+ });
943
+ for await (const event of retryGenerator) {
944
+ if (event.type === 'tool_execution_start')
945
+ args.onToolCall?.();
946
+ if (event.type === 'tool_execution_end' &&
947
+ !event.isError &&
948
+ typeof event.result === 'string') {
949
+ try {
950
+ const parsed = JSON.parse(event.result);
951
+ if ((parsed.bytesWritten ?? 0) > 0 ||
952
+ (parsed.occurrencesReplaced ?? 0) > 0) {
953
+ args.onSuccessfulWrite?.();
954
+ }
955
+ }
956
+ catch {
957
+ if (event.result.includes('bytesWritten') || event.result.includes('occurrencesReplaced')) {
958
+ args.onSuccessfulWrite?.();
959
+ }
960
+ }
961
+ }
962
+ yield event;
963
+ }
964
+ }
965
+ catch (retryErr) {
966
+ // eslint-disable-next-line no-console
967
+ console.error(`[council] ${args.chairman.id} implementation write retry failed:`, retryErr);
968
+ }
969
+ }
970
+ /** Max verify-driven delivery passes after the chairman turn (inline-js, motion, etc.). */
971
+ export const MAX_DELIVERY_ATTEMPTS = 2;
972
+ /**
973
+ * Increment 5 — verify-driven delivery loop. Re-runs deterministic verification
974
+ * and forces scoped chairman fix turns until blocking technical issues clear
975
+ * or the attempt cap is hit.
976
+ */
977
+ export async function* runChairmanDeliveryLoop(args) {
978
+ const maxAttempts = args.maxAttempts ?? MAX_DELIVERY_ATTEMPTS;
979
+ const zelariRoot = `${args.projectRoot}/.zelari`;
980
+ let attempt = 0;
981
+ while (attempt < maxAttempts) {
982
+ const report = runImplementationVerification({
983
+ projectRoot: args.projectRoot,
984
+ zelariRoot,
985
+ });
986
+ const blocking = filterDeliveryBlockingFails(report.results);
987
+ if (blocking.length === 0)
988
+ return true;
989
+ if (blocking.some((b) => b.id === 'inline-js.budget')) {
990
+ const jsFix = applyInlineJsAutofix(args.projectRoot, report);
991
+ if (jsFix.applied) {
992
+ args.onCouncilStatus?.(`[council] ${args.chairman.id} inline-js autofix: ${jsFix.fixes.join('; ')}`);
993
+ const afterJs = runImplementationVerification({
994
+ projectRoot: args.projectRoot,
995
+ zelariRoot,
996
+ });
997
+ if (filterDeliveryBlockingFails(afterJs.results).length === 0)
998
+ return true;
999
+ }
1000
+ }
1001
+ attempt++;
1002
+ const statusMsg = `[council] ${args.chairman.id} delivery pass ${attempt}/${maxAttempts}: ${blocking.map((b) => b.id).join(', ')}`;
1003
+ args.onCouncilStatus?.(statusMsg);
1004
+ // eslint-disable-next-line no-console
1005
+ console.warn(statusMsg);
1006
+ try {
1007
+ const fixGenerator = runRetryTurnForMember({
1008
+ agent: args.chairman,
1009
+ missingToolNames: ['read_file', 'edit_file'],
1010
+ minPerTool: { read_file: 3, edit_file: 10 },
1011
+ executableTools: args.executableNames,
1012
+ userMessage: args.userMessage,
1013
+ ragContext: args.config.ragContext,
1014
+ workspaceContext: args.config.workspaceContext,
1015
+ priorOutputs: args.agentOutputs,
1016
+ aiConfig: args.config.aiConfig,
1017
+ sessionId: args.sessionId,
1018
+ effectiveModel: args.effectiveModel,
1019
+ effectiveProvider: args.effectiveProvider,
1020
+ eventBus: args.config.eventBus,
1021
+ toolRegistry: args.config.tools,
1022
+ providerStream: args.config.providerStream,
1023
+ runMode: 'implementation',
1024
+ retryPrompt: buildDeliveryFixPrompt(blocking, args.userMessage),
1025
+ });
1026
+ for await (const event of fixGenerator) {
1027
+ if (event.type === 'tool_execution_start')
1028
+ args.onToolCall?.();
1029
+ yield event;
1030
+ }
1031
+ }
1032
+ catch (deliveryErr) {
1033
+ // eslint-disable-next-line no-console
1034
+ console.error(`[council] ${args.chairman.id} delivery pass ${attempt} failed:`, deliveryErr);
1035
+ break;
1036
+ }
1037
+ for (const rel of args.changedFiles) {
1038
+ for (const w of runChairmanMicroGate({ projectRoot: args.projectRoot, relPath: rel, zelariRoot })) {
1039
+ // refresh changed set — delivery may touch same targets
1040
+ args.changedFiles.add(w.file ?? rel);
1041
+ }
1042
+ }
1043
+ }
1044
+ const finalReport = runImplementationVerification({
1045
+ projectRoot: args.projectRoot,
1046
+ zelariRoot,
1047
+ });
1048
+ return filterDeliveryBlockingFails(finalReport.results).length === 0;
1049
+ }
1050
+ /**
1051
+ * Re-execute edit_file/write_file calls from a `---TOOLS---` block after the
1052
+ * chairman turn. Safety net when the harness path parsed the block but edits
1053
+ * failed (oldString drift) or the block was not fully executed.
1054
+ */
1055
+ export async function* replayChairmanTextTools(args) {
1056
+ const tools = parseTextToolCalls(args.synthesisText);
1057
+ if (tools.length === 0)
1058
+ return 0;
1059
+ let applied = 0;
1060
+ for (const tt of tools) {
1061
+ if (tt.name !== 'edit_file' && tt.name !== 'write_file')
1062
+ continue;
1063
+ const normalized = normalizeTextToolArgs(tt.name, tt.args);
1064
+ const toolCallId = `replay-${crypto.randomUUID().slice(0, 8)}`;
1065
+ yield createBrainEvent('tool_execution_start', args.sessionId, {
1066
+ toolCallId,
1067
+ toolName: tt.name,
1068
+ args: normalized,
1069
+ ...(args.memberId ? { memberId: args.memberId } : {}),
1070
+ });
1071
+ const startMs = Date.now();
1072
+ let resultStr = '';
1073
+ let isError = false;
1074
+ try {
1075
+ const result = await args.toolRegistry.invoke(tt.name, normalized, {
1076
+ cwd: args.projectRoot,
1077
+ sessionId: args.sessionId,
1078
+ });
1079
+ if (result.ok) {
1080
+ const val = result.value;
1081
+ if (tt.name === 'edit_file' && val.occurrencesReplaced === 0) {
1082
+ resultStr = `edit_file: no match for oldString (replay)`;
1083
+ isError = true;
1084
+ }
1085
+ else {
1086
+ resultStr =
1087
+ typeof result.value === 'string'
1088
+ ? result.value
1089
+ : JSON.stringify(result.value, null, 2);
1090
+ applied += 1;
1091
+ }
1092
+ }
1093
+ else {
1094
+ resultStr = result.error;
1095
+ isError = true;
1096
+ }
1097
+ }
1098
+ catch (err) {
1099
+ resultStr = err instanceof Error ? err.message : String(err);
1100
+ isError = true;
1101
+ }
1102
+ yield createBrainEvent('tool_execution_end', args.sessionId, {
1103
+ toolCallId,
1104
+ result: resultStr,
1105
+ isError,
1106
+ durationMs: Date.now() - startMs,
1107
+ });
1108
+ }
1109
+ return applied;
1110
+ }
1111
+ /**
1112
+ * Increment 4 — bounded deterministic fix loop for the chairman. When the
1113
+ * micro-gate flagged motion violations in Lucifero's writes, force a scoped
1114
+ * fix turn (read_file + edit_file only), re-scan the changed files, and repeat
1115
+ * until clean or the attempt cap. Emits only the fix turn's own events; the
1116
+ * post-council verification reports the final PASS/FAIL to the user.
1117
+ */
1118
+ export async function* runChairmanFixLoop(args) {
1119
+ const maxAttempts = args.maxAttempts ?? 3;
1120
+ const zelariRoot = `${args.projectRoot}/.zelari`;
1121
+ let current = Array.from(args.violations.values());
1122
+ let attempt = 0;
1123
+ while (current.length > 0 && attempt < maxAttempts) {
1124
+ attempt++;
1125
+ try {
1126
+ const fixGenerator = runRetryTurnForMember({
1127
+ agent: args.chairman,
1128
+ missingToolNames: ['read_file', 'edit_file'],
1129
+ minPerTool: { read_file: 2, edit_file: 8 },
1130
+ executableTools: args.executableNames,
1131
+ userMessage: args.userMessage,
1132
+ ragContext: args.config.ragContext,
1133
+ workspaceContext: args.config.workspaceContext,
1134
+ priorOutputs: args.agentOutputs,
1135
+ aiConfig: args.config.aiConfig,
1136
+ sessionId: args.sessionId,
1137
+ effectiveModel: args.effectiveModel,
1138
+ effectiveProvider: args.effectiveProvider,
1139
+ eventBus: args.config.eventBus,
1140
+ toolRegistry: args.config.tools,
1141
+ providerStream: args.config.providerStream,
1142
+ runMode: 'implementation',
1143
+ retryPrompt: buildMotionFixPrompt(current),
1144
+ });
1145
+ for await (const event of fixGenerator) {
1146
+ if (event.type === 'tool_execution_start')
1147
+ args.onToolCall?.();
1148
+ yield event;
1149
+ }
1150
+ }
1151
+ catch (fixErr) {
1152
+ // eslint-disable-next-line no-console
1153
+ console.error(`[council] chairman fix pass ${attempt} failed:`, fixErr);
1154
+ break;
1155
+ }
1156
+ // Re-scan the changed target files for the next iteration / termination.
1157
+ const rescanned = new Map();
1158
+ for (const relPath of args.changedFiles) {
1159
+ for (const w of runChairmanMicroGate({ projectRoot: args.projectRoot, relPath, zelariRoot })) {
1160
+ rescanned.set(`${w.id}|${w.file}|${w.line ?? ''}`, w);
1161
+ }
1162
+ }
1163
+ current = Array.from(rescanned.values());
1164
+ }
1165
+ }
656
1166
  /**
657
1167
  * Pure helper: given the list of tool names a member emitted during its
658
1168
  * turn, and a list of requirements, return whether all requirements are
@@ -843,7 +1353,10 @@ export async function* runRetryTurnForMember(args) {
843
1353
  const baseMessages = buildAgentMessages(args.agent, args.userMessage, args.ragContext, args.workspaceContext, args.priorOutputs, args.aiConfig, args.executableTools, args.runMode ?? 'implementation');
844
1354
  const retryMessages = [
845
1355
  ...baseMessages,
846
- { role: 'user', content: buildRetryPrompt(executableMissing) },
1356
+ {
1357
+ role: 'user',
1358
+ content: args.retryPrompt ?? buildRetryPrompt(executableMissing),
1359
+ },
847
1360
  ];
848
1361
  // Budget the retry turn so the model can satisfy ALL minimums in a
849
1362
  // single tool_calls turn. For createDocument min:1 this is 1; for