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