zelari-code 1.14.4 → 1.17.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 (58) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/app.js +8 -1
  3. package/dist/cli/app.js.map +1 -1
  4. package/dist/cli/components/StatusBar.js +5 -1
  5. package/dist/cli/components/StatusBar.js.map +1 -1
  6. package/dist/cli/councilDispatcher.js +3 -0
  7. package/dist/cli/councilDispatcher.js.map +1 -1
  8. package/dist/cli/hooks/chatStats.js +20 -2
  9. package/dist/cli/hooks/chatStats.js.map +1 -1
  10. package/dist/cli/hooks/conversationContext.js +5 -2
  11. package/dist/cli/hooks/conversationContext.js.map +1 -1
  12. package/dist/cli/hooks/historyCompaction.js +8 -1
  13. package/dist/cli/hooks/historyCompaction.js.map +1 -1
  14. package/dist/cli/hooks/useChatTurn.js +236 -60
  15. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  16. package/dist/cli/hooks/useSlashDispatch.js +42 -0
  17. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  18. package/dist/cli/main.bundled.js +2797 -863
  19. package/dist/cli/main.bundled.js.map +4 -4
  20. package/dist/cli/main.js +47 -0
  21. package/dist/cli/main.js.map +1 -1
  22. package/dist/cli/mcp/mcpManager.js +27 -6
  23. package/dist/cli/mcp/mcpManager.js.map +1 -1
  24. package/dist/cli/mcp/mcpPresets.js +77 -0
  25. package/dist/cli/mcp/mcpPresets.js.map +1 -0
  26. package/dist/cli/memory/fileBackend.js +11 -2
  27. package/dist/cli/memory/fileBackend.js.map +1 -1
  28. package/dist/cli/runHeadless.js +61 -4
  29. package/dist/cli/runHeadless.js.map +1 -1
  30. package/dist/cli/slashCommands.js +54 -1
  31. package/dist/cli/slashCommands.js.map +1 -1
  32. package/dist/cli/slashHandlers/cache.js +28 -0
  33. package/dist/cli/slashHandlers/cache.js.map +1 -0
  34. package/dist/cli/slashHandlers/state.js +83 -0
  35. package/dist/cli/slashHandlers/state.js.map +1 -0
  36. package/dist/cli/state/commitHelpers.js +62 -0
  37. package/dist/cli/state/commitHelpers.js.map +1 -0
  38. package/dist/cli/state/fileStateStore.js +262 -0
  39. package/dist/cli/state/fileStateStore.js.map +1 -0
  40. package/dist/cli/state/promptCacheStats.js +56 -0
  41. package/dist/cli/state/promptCacheStats.js.map +1 -0
  42. package/dist/cli/state/restoreState.js +70 -0
  43. package/dist/cli/state/restoreState.js.map +1 -0
  44. package/dist/cli/toolRegistry.js +22 -0
  45. package/dist/cli/toolRegistry.js.map +1 -1
  46. package/dist/cli/tools/askUser.js +71 -0
  47. package/dist/cli/tools/askUser.js.map +1 -0
  48. package/dist/cli/utils/doctor.js +19 -0
  49. package/dist/cli/utils/doctor.js.map +1 -1
  50. package/dist/cli/workspace/composeContext.js +210 -0
  51. package/dist/cli/workspace/composeContext.js.map +1 -0
  52. package/dist/cli/workspace/workspaceSummary.js +58 -31
  53. package/dist/cli/workspace/workspaceSummary.js.map +1 -1
  54. package/dist/cli/workspace/worldModel.js +331 -0
  55. package/dist/cli/workspace/worldModel.js.map +1 -0
  56. package/dist/cli/zelariMission.js +135 -18
  57. package/dist/cli/zelariMission.js.map +1 -1
  58. package/package.json +2 -2
@@ -9,7 +9,8 @@ import { resolveFailoverStream } from "../crossProviderFailover.js";
9
9
  import { resolveShell } from "@zelari/core/harness/tools/builtin/shellResolver";
10
10
  import { PROVIDERS } from "../keyStore.js";
11
11
  import { createBuiltinToolRegistry } from "../toolRegistry.js";
12
- import { buildSystemPrompt, getAllTools, SINGLE_AGENT_IDENTITY_MODULE, buildLanguagePolicyModuleFor, } from "@zelari/core/skills";
12
+ import { buildSystemPromptSplit, systemMessagesFromSplit, getAllTools, SINGLE_AGENT_IDENTITY_MODULE, buildLanguagePolicyModuleFor, } from "@zelari/core/skills";
13
+ import { hashStablePrompt } from "../state/fileStateStore.js";
13
14
  import { parseClarificationRequest, cleanAgentContent, } from "@zelari/core";
14
15
  import { appendOrExtendStreamingAssistant, appendSystem, appendToolStart, finalizeStreamingAssistant, updateToolMessageEnd, } from "./messageHelpers.js";
15
16
  import { setStreaming, finalizeStreaming, startTool, completeTool, } from "./chatState.js";
@@ -78,8 +79,45 @@ export function useChatTurn(params) {
78
79
  }
79
80
  setBusy(true);
80
81
  const workPhase = getPhase();
82
+ // Grok-style ask_user: block the tool-loop until picker resolves so
83
+ // the same harness run continues with the answer as tool_result.
84
+ const onAskUser = setPicker
85
+ ? (req) => new Promise((resolve) => {
86
+ const choices = req.choices ?? [];
87
+ if (choices.length < 2) {
88
+ resolve(null);
89
+ return;
90
+ }
91
+ setLastClarification({
92
+ question: req.question,
93
+ choices,
94
+ });
95
+ const choiceLines = choices
96
+ .map((c, i) => ` ${i + 1}. ${c}`)
97
+ .join("\n");
98
+ appendSystem(setMessages, `[in attesa di risposta — ask_user]\n${req.question}\n${choiceLines}` +
99
+ (req.context ? `\n_(${req.context})_` : "") +
100
+ "\n→ scegli dalla lista (il turno continua dopo).", Date.now());
101
+ let settled = false;
102
+ const finish = (value) => {
103
+ if (settled)
104
+ return;
105
+ settled = true;
106
+ setPicker(null);
107
+ resolve(value);
108
+ };
109
+ setPicker({
110
+ kind: "clarification",
111
+ title: req.question,
112
+ items: choices.map((c) => ({ value: c, label: c })),
113
+ onAnswer: (value) => finish(value),
114
+ onCancel: () => finish(null),
115
+ });
116
+ })
117
+ : undefined;
81
118
  const { registry: toolRegistry } = createBuiltinToolRegistry({
82
119
  planMode: workPhase === "plan",
120
+ onAskUser,
83
121
  });
84
122
  const baseProviderStream = openaiCompatibleProvider(envConfig);
85
123
  const failoverResolution = await resolveFailoverStream({
@@ -111,17 +149,26 @@ export function useChatTurn(params) {
111
149
  // The plan lives in .zelari/plan.json but the agent had no idea it
112
150
  // existed — users had to paste task-file paths by hand. Best-effort:
113
151
  // no plan → null → zero prompt-token cost.
114
- let planSummary = null;
115
- // v0.7.4: give the single agent the same project awareness the council
116
- // has (tech stack, file layout, scripts) plus a pointer to the council
117
- // workspace so it reads .zelari/plan.json with its own tools.
118
- let workspaceSummary = null;
119
- let zelariReadHint = "";
152
+ // v1.16: unified compose — product truth + draft plan ops; plan is
153
+ // NEVER mislabeled as ragContext (that slot is for real memory only).
154
+ let composedWorkspace = "";
155
+ let composedInstructions = "";
156
+ let hasPlan = false;
120
157
  try {
121
- const { buildPlanSummary, buildWorkspaceSummary, buildZelariReadHint, } = await import("../workspace/workspaceSummary.js");
122
- planSummary = buildPlanSummary(cwd);
123
- workspaceSummary = buildWorkspaceSummary(cwd);
124
- zelariReadHint = buildZelariReadHint(cwd);
158
+ const { composeProjectContext } = await import("../workspace/composeContext.js");
159
+ const { hasWorkspacePlan } = await import("../workspace/planDetect.js");
160
+ hasPlan = hasWorkspacePlan(cwd);
161
+ const composed = composeProjectContext({
162
+ mode: "agent",
163
+ cwd,
164
+ userMessage: userText,
165
+ includeLessons: false,
166
+ });
167
+ composedWorkspace = composed.workspaceContext;
168
+ composedInstructions = composed.projectInstructions;
169
+ for (const w of composed.warnings) {
170
+ appendSystem(setMessages, w, Date.now());
171
+ }
125
172
  // v0.7.4: close the plan loop. The single agent implements the tasks
126
173
  // the council planned, but had no official way to advance their
127
174
  // status — it would have to hand-edit plan.json with write_file
@@ -133,7 +180,7 @@ export function useChatTurn(params) {
133
180
  // requires (opts.requiredTools), mapping the Electron-era `searchRAG`
134
181
  // to the CLI's `searchDocuments`.
135
182
  const wantedWorkspaceTools = new Set();
136
- if (planSummary)
183
+ if (hasPlan)
137
184
  wantedWorkspaceTools.add("updateTask");
138
185
  const WORKSPACE_STUB_NAMES = new Set([
139
186
  "createPhase",
@@ -211,7 +258,6 @@ export function useChatTurn(params) {
211
258
  // not "member of an AI Council".
212
259
  const planPhaseBlock = workPhase === "plan"
213
260
  ? [
214
- "",
215
261
  "# Work Phase: PLAN",
216
262
  "You are in PLAN mode. Explore and design only.",
217
263
  "- Do NOT implement production code or run destructive shell commands.",
@@ -221,39 +267,42 @@ export function useChatTurn(params) {
221
267
  ].join("\n")
222
268
  : workPhase === "build"
223
269
  ? [
224
- "",
225
270
  "# Work Phase: BUILD",
226
271
  "Implement on disk. Prefer acting over describing.",
227
272
  "- Prior plan/synthesis text is a SPEC to apply — not proof files already changed.",
228
273
  "- You MUST use write_file/edit_file for every file you change before claiming done.",
229
274
  "- After read_file: if the planned change is missing, WRITE it — do not stop at analysis.",
230
275
  "- Never claim already-implemented based only on reading a plan or skimming code.",
231
- planSummary
232
- ? "- An approved plan exists in the workspace — implement it and update task statuses as you go."
276
+ hasPlan
277
+ ? "- A draft plan exists under .zelari/ — implement grounded tasks and update statuses; do not treat design docs as shipped code."
233
278
  : "",
234
279
  ]
235
280
  .filter(Boolean)
236
281
  .join("\n")
237
282
  : "";
283
+ // Stable platform/shell block (session-constant). Phase/plan go in volatile.
238
284
  const shellContextBlock = [
239
285
  "# Platform & Shell",
240
286
  `platform: ${process.platform}`,
241
287
  `shell: ${resolvedShell.via}`,
242
288
  shellGuidance,
243
289
  nonInteractiveGuidance,
244
- planPhaseBlock,
245
290
  "",
246
291
  "# Working Directory",
247
292
  `You are running in: ${cwd}`,
248
293
  "All relative file paths are resolved against this directory. Always work with real files here.",
249
- ...(planSummary
294
+ ].join("\n");
295
+ const volatileSessionBlock = [
296
+ planPhaseBlock,
297
+ hasPlan
250
298
  ? [
251
- "",
252
299
  "# Plan Tracking",
253
- '- Plan tasks: when you START working on a plan task call updateTask {taskId, status: "in_progress"}; when it is complete and verified call updateTask {taskId, status: "done"}. NEVER edit .zelari/plan.json by hand.',
254
- ]
255
- : []),
256
- ].join("\n");
300
+ '- Plan tasks (draft ops): when you START a task call updateTask {taskId, status: "in_progress"}; when complete and verified call updateTask {taskId, status: "done"}. NEVER edit .zelari/plan.json by hand. Prefer product tree over design docs.',
301
+ ].join("\n")
302
+ : "",
303
+ ]
304
+ .filter(Boolean)
305
+ .join("\n\n");
257
306
  const singleAgentRole = {
258
307
  id: "single",
259
308
  name: "Zelari Code",
@@ -264,17 +313,23 @@ export function useChatTurn(params) {
264
313
  tools: toolListNames,
265
314
  systemPrompt: shellContextBlock,
266
315
  };
267
- // workspaceContext + ragContext carry the summaries that were inline
268
- // before; the builder places them under # Current Workspace State and
269
- // # Retrieved Knowledge (RAG) headers.
270
- const workspaceContext = [workspaceSummary, zelariReadHint]
316
+ // Composed workspace already includes product tree + draft plan ops +
317
+ // epistemic banner. ragContext stays empty on the agent path (memory
318
+ // is zelari-only) so plan is never mislabeled as "Retrieved Knowledge".
319
+ const workspaceContext = [
320
+ volatileSessionBlock,
321
+ composedWorkspace,
322
+ ]
271
323
  .filter(Boolean)
272
324
  .join("\n\n");
273
325
  // v1.5.3: build via the shared builder. Wrap in try/catch with a
274
326
  // minimal fallback so a builder failure (e.g. test context without a
275
327
  // populated catalog) never breaks dispatch — the harness still gets a
276
328
  // usable system prompt and the turn proceeds.
277
- let systemPrompt;
329
+ // Cache Wars: stable/volatile split so plan/workspace updates do not
330
+ // bust the cached prefix (identity + tools + platform).
331
+ let systemMessages = [];
332
+ let lastStableHash;
278
333
  try {
279
334
  // v1.7.0: detect the user's language and inject the language-policy
280
335
  // module so the agent replies in the user's language. Honors
@@ -283,13 +338,11 @@ export function useChatTurn(params) {
283
338
  // — it lives in priority space (5) so it sorts BEFORE the base-identity
284
339
  // module (10): the model sets language scaffolding before reading role text.
285
340
  const languageModule = buildLanguagePolicyModuleFor(userText);
286
- const { loadProjectInstructions } = await import("../workspace/projectInstructions.js");
287
- const projectInstructions = loadProjectInstructions(process.cwd()).content;
288
- systemPrompt = buildSystemPrompt(singleAgentRole, {
341
+ const split = buildSystemPromptSplit(singleAgentRole, {
289
342
  tools: getAllTools(),
290
343
  toolNames: toolListNames,
291
344
  mode: "agent",
292
- projectInstructions: projectInstructions || undefined,
345
+ projectInstructions: composedInstructions || undefined,
293
346
  aiConfig: {
294
347
  enabledSkills: [],
295
348
  enabledTools: toolListNames,
@@ -300,14 +353,18 @@ export function useChatTurn(params) {
300
353
  agentSkillConfigs: [],
301
354
  },
302
355
  workspaceContext: workspaceContext || undefined,
303
- ragContext: planSummary || undefined,
356
+ // Do NOT put plan text here — that was mislabeled as RAG and
357
+ // taught models to treat design vault as retrieved knowledge.
358
+ ragContext: undefined,
304
359
  });
360
+ lastStableHash = hashStablePrompt(split.stable);
361
+ systemMessages = systemMessagesFromSplit(split);
305
362
  }
306
363
  catch {
307
364
  // Fallback: identity + platform/shell + tool list. Keeps the turn
308
365
  // runnable even if the builder or catalog is unavailable.
309
366
  const languageModule = buildLanguagePolicyModuleFor(userText);
310
- systemPrompt = [
367
+ const fallback = [
311
368
  SINGLE_AGENT_IDENTITY_MODULE.content,
312
369
  languageModule.content,
313
370
  shellContextBlock,
@@ -315,8 +372,9 @@ export function useChatTurn(params) {
315
372
  "You can call these tools. Use them to take action and gather information autonomously:",
316
373
  toolList,
317
374
  ...(workspaceContext ? ["", workspaceContext] : []),
318
- ...(planSummary ? ["", planSummary] : []),
319
375
  ].join("\n");
376
+ lastStableHash = hashStablePrompt(fallback);
377
+ systemMessages = [{ role: "system", content: fallback }];
320
378
  }
321
379
  // v0.7.1 (A2): per-turn tool-call budget for single-prompt turns.
322
380
  // The council sets 5; the single-prompt path previously set NONE, so a
@@ -338,7 +396,7 @@ export function useChatTurn(params) {
338
396
  model: envConfig.model,
339
397
  provider: "openai-compatible",
340
398
  messages: [
341
- { role: "system", content: systemPrompt },
399
+ ...systemMessages,
342
400
  // v1.8.0: shared rolling history (agent/council/zelari) so short
343
401
  // answers bind to prior ---QUESTION--- blocks. Possibly empty
344
402
  // when ZELARI_HISTORY_TURNS=0.
@@ -559,6 +617,14 @@ export function useChatTurn(params) {
559
617
  question: clar.question,
560
618
  choices: clar.choices,
561
619
  });
620
+ // Visible prompt while the model "waits": turn is finished,
621
+ // busy=false, SelectList (or typed short answer) continues.
622
+ const choiceLines = clar.choices
623
+ .map((c, i) => ` ${i + 1}. ${c}`)
624
+ .join("\n");
625
+ appendSystem(setMessages, `[in attesa di risposta]\n${clar.question}\n${choiceLines}` +
626
+ (clar.context ? `\n_(${clar.context})_` : "") +
627
+ "\n→ scegli dalla lista oppure digita la risposta e invia.", Date.now());
562
628
  if (setPicker) {
563
629
  setPicker({
564
630
  kind: "clarification",
@@ -567,6 +633,9 @@ export function useChatTurn(params) {
567
633
  onAnswer: (value) => {
568
634
  void dispatchPrompt(value);
569
635
  },
636
+ onCancel: () => {
637
+ appendSystem(setMessages, "[chiarimento annullato — puoi ancora rispondere scrivendo in chat]", Date.now());
638
+ },
570
639
  });
571
640
  }
572
641
  }
@@ -581,7 +650,7 @@ export function useChatTurn(params) {
581
650
  harnessRef.current = null;
582
651
  setQueueCount(0);
583
652
  setBusy(false);
584
- setSessionStats((prev) => computeSessionStatsDelta(realUsage, userText, assistantContent, envConfig.model, prev));
653
+ setSessionStats((prev) => computeSessionStatsDelta(realUsage, userText, assistantContent, envConfig.model, prev, lastStableHash ? { stableHash: lastStableHash } : undefined));
585
654
  }
586
655
  }
587
656
  catch (err) {
@@ -694,12 +763,46 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
694
763
  const { createWorkspaceToolRegistry } = await import("../workspace/toolRegistry.js");
695
764
  const { setWorkspaceStubs } = await import("@zelari/core/skills");
696
765
  const { runPostCouncilHook } = await import("../workspace/postCouncilHook.js");
697
- const { buildWorkspaceSummary, buildPlanSummary } = await import("../workspace/workspaceSummary.js");
698
- const { buildLessonsSummary } = await import("../workspace/buildLessonsSummary.js");
766
+ const { composeProjectContext } = await import("../workspace/composeContext.js");
699
767
  const { FeedbackStore } = await import("../councilFeedback.js");
700
768
  const workPhase = getPhase();
769
+ const onAskUserCouncil = setPicker
770
+ ? (req) => new Promise((resolve) => {
771
+ const choices = req.choices ?? [];
772
+ if (choices.length < 2) {
773
+ resolve(null);
774
+ return;
775
+ }
776
+ setLastClarification({
777
+ question: req.question,
778
+ choices,
779
+ });
780
+ const choiceLines = choices
781
+ .map((c, i) => ` ${i + 1}. ${c}`)
782
+ .join("\n");
783
+ appendSystem(setMessages, `[council in attesa — ask_user]\n${req.question}\n${choiceLines}` +
784
+ (req.context ? `\n_(${req.context})_` : "") +
785
+ "\n→ scegli dalla lista (il membro riprende dopo la risposta).", Date.now());
786
+ let settled = false;
787
+ const finish = (value) => {
788
+ if (settled)
789
+ return;
790
+ settled = true;
791
+ setPicker(null);
792
+ resolve(value);
793
+ };
794
+ setPicker({
795
+ kind: "clarification",
796
+ title: req.question,
797
+ items: choices.map((c) => ({ value: c, label: c })),
798
+ onAnswer: (value) => finish(value),
799
+ onCancel: () => finish(null),
800
+ });
801
+ })
802
+ : undefined;
701
803
  const { registry: councilToolRegistry } = createBuiltinToolRegistry({
702
804
  planMode: workPhase === "plan",
805
+ onAskUser: onAskUserCouncil,
703
806
  });
704
807
  const workspaceCtx = createWorkspaceContext();
705
808
  const workspaceReg = createWorkspaceToolRegistry(workspaceCtx);
@@ -718,7 +821,10 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
718
821
  // single-agent path — zero extra spawns).
719
822
  try {
720
823
  const { registerMcpTools } = await import("../mcp/mcpManager.js");
721
- const mcp = await registerMcpTools(councilToolRegistry);
824
+ // Council: skip Cua desktop tools by default (context hygiene).
825
+ const mcp = await registerMcpTools(councilToolRegistry, process.cwd(), {
826
+ councilMode: true,
827
+ });
722
828
  for (const w of mcp.warnings)
723
829
  appendSystem(setMessages, w);
724
830
  }
@@ -745,6 +851,11 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
745
851
  default: 15,
746
852
  min: 1,
747
853
  });
854
+ // Wire soft/hard tool-loop budgets into every council member harness
855
+ // (previously only the single-agent path set these — members defaulted
856
+ // to harness soft=30 with unbounded soft×3 hard extension).
857
+ const councilMaxToolLoop = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 30, min: 1 });
858
+ const councilMaxToolLoopHard = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_HARD, { default: 0, min: 0 });
748
859
  // v0.7.1 (A3): track member completion so the AGENTS.MD hook only runs when
749
860
  // the council actually produced output. v0.7.1 (A4): track repeated provider
750
861
  // errors to abort the remaining members instead of grinding through every
@@ -772,30 +883,46 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
772
883
  sessionId,
773
884
  tools: councilToolRegistry,
774
885
  feedbackStore: councilFeedbackStore,
775
- // v0.7.2 (B2): give the council the same project awareness the
776
- // single-prompt path has cwd, tech stack, file layout, build scripts.
777
- // Without this, members had no idea which project they were operating
778
- // on and projected their identity onto the task.
779
- // v0.7.3: append the existing plan (if any) so a follow-up /council
780
- // continues it instead of re-planning from scratch.
781
- // v1.8.0: rolling conversation context so short answers bind across
782
- // council turns (same history store as the single-agent path).
783
- workspaceContext: [
784
- buildWorkspaceSummary(process.cwd()),
785
- buildPlanSummary(process.cwd(), { userMessage: effectiveText }),
786
- buildLessonsSummary(process.cwd(), effectiveText),
787
- formatHistoryForCouncil(4),
788
- ]
789
- .filter(Boolean)
790
- .join("\n\n"),
886
+ // v1.16: unified compose product truth + draft plan ops + design index
887
+ // (not full vault). Memory RAG only via overrides.ragContext.
888
+ // Fail-soft: never block a council run if compose throws in tests/odd cwd.
889
+ ...(() => {
890
+ try {
891
+ const composed = composeProjectContext({
892
+ mode: overrides.ragContext ? "zelari" : "council",
893
+ cwd: process.cwd(),
894
+ userMessage: effectiveText,
895
+ memoryHits: overrides.ragContext,
896
+ historySnippet: formatHistoryForCouncil(4) || undefined,
897
+ includeLessons: true,
898
+ });
899
+ for (const w of composed.warnings) {
900
+ appendSystem(setMessages, w, Date.now());
901
+ }
902
+ return {
903
+ workspaceContext: composed.workspaceContext,
904
+ ...(composed.ragContext ? { ragContext: composed.ragContext } : {}),
905
+ };
906
+ }
907
+ catch {
908
+ return {
909
+ workspaceContext: formatHistoryForCouncil(4) || "",
910
+ ...(overrides.ragContext
911
+ ? { ragContext: overrides.ragContext }
912
+ : {}),
913
+ };
914
+ }
915
+ })(),
791
916
  maxToolCallsPerTurn: councilMaxToolCalls,
792
- // v1.0: Zelari-mode per-slice overrides (memory RAG, forced run mode,
793
- // raised chairman budget). No-ops for a normal /council run.
794
- ...(overrides.ragContext ? { ragContext: overrides.ragContext } : {}),
917
+ maxToolLoopIterations: councilMaxToolLoop,
918
+ ...(councilMaxToolLoopHard > 0
919
+ ? { maxToolLoopHardCap: councilMaxToolLoopHard }
920
+ : {}),
795
921
  runMode: phaseRunMode,
796
922
  ...(overrides.maxToolCallsChairman
797
923
  ? { maxToolCallsChairman: overrides.maxToolCallsChairman }
798
924
  : {}),
925
+ ...(overrides.skipSpecialists ? { skipSpecialists: true } : {}),
799
926
  onCouncilStatus: (message) => {
800
927
  appendSystem(setMessages, message, Date.now());
801
928
  },
@@ -811,6 +938,12 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
811
938
  question: req.question,
812
939
  choices,
813
940
  });
941
+ const choiceLines = choices
942
+ .map((c, i) => ` ${i + 1}. ${c}`)
943
+ .join("\n");
944
+ appendSystem(setMessages, `[council in attesa]\n${req.question}\n${choiceLines}` +
945
+ (req.context ? `\n_(${req.context})_` : "") +
946
+ "\n→ scegli dalla lista (il membro riprende dopo la risposta).", Date.now());
814
947
  let settled = false;
815
948
  const finish = (value) => {
816
949
  if (settled)
@@ -1083,6 +1216,48 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
1083
1216
  appendSystem(setMessages, `[completion] readyToCommit=false — ${n} blocking issue(s)${c.degraded ? " (degraded run)" : ""}`, Date.now());
1084
1217
  }
1085
1218
  }
1219
+ // Durable State Commit after verified council completion (Palmer).
1220
+ // Skip degraded / failed verification — those must not become HEAD.
1221
+ const verifyOk = hook.verification?.ran
1222
+ ? hook.verification.ok === true
1223
+ : false;
1224
+ const completionOk = hook.completion?.completion?.ok === true ||
1225
+ hook.completion?.completion?.readyToCommit === true;
1226
+ if (!degraded.degraded &&
1227
+ !councilAborted &&
1228
+ (verifyOk || completionOk) &&
1229
+ luciferWriteCount > 0) {
1230
+ try {
1231
+ const { tryStateCommit, discoveriesFromOutcome } = await import("../state/commitHelpers.js");
1232
+ const res = await tryStateCommit({
1233
+ projectRoot: process.cwd(),
1234
+ mode: "council",
1235
+ layer: `council:${councilRunMode ?? "run"}`,
1236
+ label: `council ${councilRunMode ?? "run"} verified`,
1237
+ sessionId,
1238
+ verification: {
1239
+ ok: true,
1240
+ ran: hook.verification?.ran ?? completionOk,
1241
+ },
1242
+ withCheckpoint: true,
1243
+ discoveries: discoveriesFromOutcome({
1244
+ stepId: `${sessionId}-${Date.now()}`,
1245
+ synthesis: chairmanSynthesisText,
1246
+ writeCount: luciferWriteCount,
1247
+ note: "Council implementation verified",
1248
+ }),
1249
+ });
1250
+ if (res.ok && res.meta?.id) {
1251
+ appendSystem(setMessages, `[state] commit ${res.meta.id}` +
1252
+ (res.checkpointId
1253
+ ? ` · checkpoint ${res.checkpointId}`
1254
+ : ""), Date.now());
1255
+ }
1256
+ }
1257
+ catch {
1258
+ // fail-open
1259
+ }
1260
+ }
1086
1261
  }
1087
1262
  catch {
1088
1263
  // Best-effort — never block on AGENTS.MD errors.
@@ -1171,11 +1346,12 @@ async function runZelariMissionInTui(userMessage, deps, emit) {
1171
1346
  projectRoot,
1172
1347
  memory,
1173
1348
  emit,
1174
- runSlice: async ({ userMessage: slicePrompt, runMode, ragContext }) => {
1349
+ runSlice: async ({ userMessage: slicePrompt, runMode, ragContext, implementerRetry, }) => {
1175
1350
  const r = await dispatchCouncilPromptImpl(slicePrompt, deps, {
1176
1351
  ragContext,
1177
1352
  runMode,
1178
1353
  maxToolCallsChairman: chairmanBudget,
1354
+ ...(implementerRetry ? { skipSpecialists: true } : {}),
1179
1355
  });
1180
1356
  return {
1181
1357
  completionOk: r.completionOk,