zelari-code 1.15.0 → 1.17.1

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 +36 -11
  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 +254 -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 +2509 -845
  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 +11 -1
  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 +79 -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 +35 -0
  33. package/dist/cli/slashHandlers/cache.js.map +1 -0
  34. package/dist/cli/slashHandlers/state.js +97 -0
  35. package/dist/cli/slashHandlers/state.js.map +1 -0
  36. package/dist/cli/state/commitHelpers.js +73 -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/loadDurableContext.js +42 -0
  41. package/dist/cli/state/loadDurableContext.js.map +1 -0
  42. package/dist/cli/state/promptCacheStats.js +56 -0
  43. package/dist/cli/state/promptCacheStats.js.map +1 -0
  44. package/dist/cli/state/restoreState.js +70 -0
  45. package/dist/cli/state/restoreState.js.map +1 -0
  46. package/dist/cli/toolRegistry.js +8 -0
  47. package/dist/cli/toolRegistry.js.map +1 -1
  48. package/dist/cli/tools/askUser.js +71 -0
  49. package/dist/cli/tools/askUser.js.map +1 -0
  50. package/dist/cli/utils/doctor.js +19 -0
  51. package/dist/cli/utils/doctor.js.map +1 -1
  52. package/dist/cli/workspace/composeContext.js +216 -0
  53. package/dist/cli/workspace/composeContext.js.map +1 -0
  54. package/dist/cli/workspace/workspaceSummary.js +58 -31
  55. package/dist/cli/workspace/workspaceSummary.js.map +1 -1
  56. package/dist/cli/zelariMission.js +127 -17
  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,39 @@ 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 { loadDurableContext } = await import("../state/loadDurableContext.js");
162
+ const durableState = await loadDurableContext(cwd);
163
+ const composed = composeProjectContext({
164
+ mode: "agent",
165
+ cwd,
166
+ userMessage: userText,
167
+ includeLessons: false,
168
+ durableState: durableState || undefined,
169
+ // Pre-loaded async — skip sync fallback double-read.
170
+ includeDurableState: false,
171
+ });
172
+ composedWorkspace = composed.workspaceContext;
173
+ composedInstructions = composed.projectInstructions;
174
+ // Put durable into volatile workspace path via rag if compose put it
175
+ // in ragContext — agent buildSystemPrompt uses workspaceContext only;
176
+ // merge rag durable into workspace for agent visibility.
177
+ if (composed.ragContext) {
178
+ composedWorkspace = [composedWorkspace, composed.ragContext]
179
+ .filter(Boolean)
180
+ .join("\n\n");
181
+ }
182
+ for (const w of composed.warnings) {
183
+ appendSystem(setMessages, w, Date.now());
184
+ }
125
185
  // v0.7.4: close the plan loop. The single agent implements the tasks
126
186
  // the council planned, but had no official way to advance their
127
187
  // status — it would have to hand-edit plan.json with write_file
@@ -133,7 +193,7 @@ export function useChatTurn(params) {
133
193
  // requires (opts.requiredTools), mapping the Electron-era `searchRAG`
134
194
  // to the CLI's `searchDocuments`.
135
195
  const wantedWorkspaceTools = new Set();
136
- if (planSummary)
196
+ if (hasPlan)
137
197
  wantedWorkspaceTools.add("updateTask");
138
198
  const WORKSPACE_STUB_NAMES = new Set([
139
199
  "createPhase",
@@ -211,7 +271,6 @@ export function useChatTurn(params) {
211
271
  // not "member of an AI Council".
212
272
  const planPhaseBlock = workPhase === "plan"
213
273
  ? [
214
- "",
215
274
  "# Work Phase: PLAN",
216
275
  "You are in PLAN mode. Explore and design only.",
217
276
  "- Do NOT implement production code or run destructive shell commands.",
@@ -221,39 +280,42 @@ export function useChatTurn(params) {
221
280
  ].join("\n")
222
281
  : workPhase === "build"
223
282
  ? [
224
- "",
225
283
  "# Work Phase: BUILD",
226
284
  "Implement on disk. Prefer acting over describing.",
227
285
  "- Prior plan/synthesis text is a SPEC to apply — not proof files already changed.",
228
286
  "- You MUST use write_file/edit_file for every file you change before claiming done.",
229
287
  "- After read_file: if the planned change is missing, WRITE it — do not stop at analysis.",
230
288
  "- 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."
289
+ hasPlan
290
+ ? "- A draft plan exists under .zelari/ — implement grounded tasks and update statuses; do not treat design docs as shipped code."
233
291
  : "",
234
292
  ]
235
293
  .filter(Boolean)
236
294
  .join("\n")
237
295
  : "";
296
+ // Stable platform/shell block (session-constant). Phase/plan go in volatile.
238
297
  const shellContextBlock = [
239
298
  "# Platform & Shell",
240
299
  `platform: ${process.platform}`,
241
300
  `shell: ${resolvedShell.via}`,
242
301
  shellGuidance,
243
302
  nonInteractiveGuidance,
244
- planPhaseBlock,
245
303
  "",
246
304
  "# Working Directory",
247
305
  `You are running in: ${cwd}`,
248
306
  "All relative file paths are resolved against this directory. Always work with real files here.",
249
- ...(planSummary
307
+ ].join("\n");
308
+ const volatileSessionBlock = [
309
+ planPhaseBlock,
310
+ hasPlan
250
311
  ? [
251
- "",
252
312
  "# 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");
313
+ '- 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.',
314
+ ].join("\n")
315
+ : "",
316
+ ]
317
+ .filter(Boolean)
318
+ .join("\n\n");
257
319
  const singleAgentRole = {
258
320
  id: "single",
259
321
  name: "Zelari Code",
@@ -264,17 +326,23 @@ export function useChatTurn(params) {
264
326
  tools: toolListNames,
265
327
  systemPrompt: shellContextBlock,
266
328
  };
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]
329
+ // Composed workspace already includes product tree + draft plan ops +
330
+ // epistemic banner. ragContext stays empty on the agent path (memory
331
+ // is zelari-only) so plan is never mislabeled as "Retrieved Knowledge".
332
+ const workspaceContext = [
333
+ volatileSessionBlock,
334
+ composedWorkspace,
335
+ ]
271
336
  .filter(Boolean)
272
337
  .join("\n\n");
273
338
  // v1.5.3: build via the shared builder. Wrap in try/catch with a
274
339
  // minimal fallback so a builder failure (e.g. test context without a
275
340
  // populated catalog) never breaks dispatch — the harness still gets a
276
341
  // usable system prompt and the turn proceeds.
277
- let systemPrompt;
342
+ // Cache Wars: stable/volatile split so plan/workspace updates do not
343
+ // bust the cached prefix (identity + tools + platform).
344
+ let systemMessages = [];
345
+ let lastStableHash;
278
346
  try {
279
347
  // v1.7.0: detect the user's language and inject the language-policy
280
348
  // module so the agent replies in the user's language. Honors
@@ -283,13 +351,11 @@ export function useChatTurn(params) {
283
351
  // — it lives in priority space (5) so it sorts BEFORE the base-identity
284
352
  // module (10): the model sets language scaffolding before reading role text.
285
353
  const languageModule = buildLanguagePolicyModuleFor(userText);
286
- const { loadProjectInstructions } = await import("../workspace/projectInstructions.js");
287
- const projectInstructions = loadProjectInstructions(process.cwd()).content;
288
- systemPrompt = buildSystemPrompt(singleAgentRole, {
354
+ const split = buildSystemPromptSplit(singleAgentRole, {
289
355
  tools: getAllTools(),
290
356
  toolNames: toolListNames,
291
357
  mode: "agent",
292
- projectInstructions: projectInstructions || undefined,
358
+ projectInstructions: composedInstructions || undefined,
293
359
  aiConfig: {
294
360
  enabledSkills: [],
295
361
  enabledTools: toolListNames,
@@ -300,14 +366,18 @@ export function useChatTurn(params) {
300
366
  agentSkillConfigs: [],
301
367
  },
302
368
  workspaceContext: workspaceContext || undefined,
303
- ragContext: planSummary || undefined,
369
+ // Do NOT put plan text here — that was mislabeled as RAG and
370
+ // taught models to treat design vault as retrieved knowledge.
371
+ ragContext: undefined,
304
372
  });
373
+ lastStableHash = hashStablePrompt(split.stable);
374
+ systemMessages = systemMessagesFromSplit(split);
305
375
  }
306
376
  catch {
307
377
  // Fallback: identity + platform/shell + tool list. Keeps the turn
308
378
  // runnable even if the builder or catalog is unavailable.
309
379
  const languageModule = buildLanguagePolicyModuleFor(userText);
310
- systemPrompt = [
380
+ const fallback = [
311
381
  SINGLE_AGENT_IDENTITY_MODULE.content,
312
382
  languageModule.content,
313
383
  shellContextBlock,
@@ -315,8 +385,9 @@ export function useChatTurn(params) {
315
385
  "You can call these tools. Use them to take action and gather information autonomously:",
316
386
  toolList,
317
387
  ...(workspaceContext ? ["", workspaceContext] : []),
318
- ...(planSummary ? ["", planSummary] : []),
319
388
  ].join("\n");
389
+ lastStableHash = hashStablePrompt(fallback);
390
+ systemMessages = [{ role: "system", content: fallback }];
320
391
  }
321
392
  // v0.7.1 (A2): per-turn tool-call budget for single-prompt turns.
322
393
  // The council sets 5; the single-prompt path previously set NONE, so a
@@ -338,7 +409,7 @@ export function useChatTurn(params) {
338
409
  model: envConfig.model,
339
410
  provider: "openai-compatible",
340
411
  messages: [
341
- { role: "system", content: systemPrompt },
412
+ ...systemMessages,
342
413
  // v1.8.0: shared rolling history (agent/council/zelari) so short
343
414
  // answers bind to prior ---QUESTION--- blocks. Possibly empty
344
415
  // when ZELARI_HISTORY_TURNS=0.
@@ -559,6 +630,14 @@ export function useChatTurn(params) {
559
630
  question: clar.question,
560
631
  choices: clar.choices,
561
632
  });
633
+ // Visible prompt while the model "waits": turn is finished,
634
+ // busy=false, SelectList (or typed short answer) continues.
635
+ const choiceLines = clar.choices
636
+ .map((c, i) => ` ${i + 1}. ${c}`)
637
+ .join("\n");
638
+ appendSystem(setMessages, `[in attesa di risposta]\n${clar.question}\n${choiceLines}` +
639
+ (clar.context ? `\n_(${clar.context})_` : "") +
640
+ "\n→ scegli dalla lista oppure digita la risposta e invia.", Date.now());
562
641
  if (setPicker) {
563
642
  setPicker({
564
643
  kind: "clarification",
@@ -567,6 +646,9 @@ export function useChatTurn(params) {
567
646
  onAnswer: (value) => {
568
647
  void dispatchPrompt(value);
569
648
  },
649
+ onCancel: () => {
650
+ appendSystem(setMessages, "[chiarimento annullato — puoi ancora rispondere scrivendo in chat]", Date.now());
651
+ },
570
652
  });
571
653
  }
572
654
  }
@@ -581,7 +663,7 @@ export function useChatTurn(params) {
581
663
  harnessRef.current = null;
582
664
  setQueueCount(0);
583
665
  setBusy(false);
584
- setSessionStats((prev) => computeSessionStatsDelta(realUsage, userText, assistantContent, envConfig.model, prev));
666
+ setSessionStats((prev) => computeSessionStatsDelta(realUsage, userText, assistantContent, envConfig.model, prev, lastStableHash ? { stableHash: lastStableHash } : undefined));
585
667
  }
586
668
  }
587
669
  catch (err) {
@@ -694,12 +776,46 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
694
776
  const { createWorkspaceToolRegistry } = await import("../workspace/toolRegistry.js");
695
777
  const { setWorkspaceStubs } = await import("@zelari/core/skills");
696
778
  const { runPostCouncilHook } = await import("../workspace/postCouncilHook.js");
697
- const { buildWorkspaceSummary, buildPlanSummary } = await import("../workspace/workspaceSummary.js");
698
- const { buildLessonsSummary } = await import("../workspace/buildLessonsSummary.js");
779
+ const { composeProjectContext } = await import("../workspace/composeContext.js");
699
780
  const { FeedbackStore } = await import("../councilFeedback.js");
700
781
  const workPhase = getPhase();
782
+ const onAskUserCouncil = setPicker
783
+ ? (req) => new Promise((resolve) => {
784
+ const choices = req.choices ?? [];
785
+ if (choices.length < 2) {
786
+ resolve(null);
787
+ return;
788
+ }
789
+ setLastClarification({
790
+ question: req.question,
791
+ choices,
792
+ });
793
+ const choiceLines = choices
794
+ .map((c, i) => ` ${i + 1}. ${c}`)
795
+ .join("\n");
796
+ appendSystem(setMessages, `[council in attesa — ask_user]\n${req.question}\n${choiceLines}` +
797
+ (req.context ? `\n_(${req.context})_` : "") +
798
+ "\n→ scegli dalla lista (il membro riprende dopo la risposta).", Date.now());
799
+ let settled = false;
800
+ const finish = (value) => {
801
+ if (settled)
802
+ return;
803
+ settled = true;
804
+ setPicker(null);
805
+ resolve(value);
806
+ };
807
+ setPicker({
808
+ kind: "clarification",
809
+ title: req.question,
810
+ items: choices.map((c) => ({ value: c, label: c })),
811
+ onAnswer: (value) => finish(value),
812
+ onCancel: () => finish(null),
813
+ });
814
+ })
815
+ : undefined;
701
816
  const { registry: councilToolRegistry } = createBuiltinToolRegistry({
702
817
  planMode: workPhase === "plan",
818
+ onAskUser: onAskUserCouncil,
703
819
  });
704
820
  const workspaceCtx = createWorkspaceContext();
705
821
  const workspaceReg = createWorkspaceToolRegistry(workspaceCtx);
@@ -718,7 +834,10 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
718
834
  // single-agent path — zero extra spawns).
719
835
  try {
720
836
  const { registerMcpTools } = await import("../mcp/mcpManager.js");
721
- const mcp = await registerMcpTools(councilToolRegistry);
837
+ // Council: skip Cua desktop tools by default (context hygiene).
838
+ const mcp = await registerMcpTools(councilToolRegistry, process.cwd(), {
839
+ councilMode: true,
840
+ });
722
841
  for (const w of mcp.warnings)
723
842
  appendSystem(setMessages, w);
724
843
  }
@@ -745,6 +864,11 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
745
864
  default: 15,
746
865
  min: 1,
747
866
  });
867
+ // Wire soft/hard tool-loop budgets into every council member harness
868
+ // (previously only the single-agent path set these — members defaulted
869
+ // to harness soft=30 with unbounded soft×3 hard extension).
870
+ const councilMaxToolLoop = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 30, min: 1 });
871
+ const councilMaxToolLoopHard = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_HARD, { default: 0, min: 0 });
748
872
  // v0.7.1 (A3): track member completion so the AGENTS.MD hook only runs when
749
873
  // the council actually produced output. v0.7.1 (A4): track repeated provider
750
874
  // errors to abort the remaining members instead of grinding through every
@@ -763,6 +887,37 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
763
887
  let sliceRan = false;
764
888
  let sliceDegraded = false;
765
889
  const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
890
+ // Pre-load durable HEAD once (async materialize) for all council members.
891
+ let councilCompose = { workspaceContext: formatHistoryForCouncil(4) || "" };
892
+ try {
893
+ const { loadDurableContext } = await import("../state/loadDurableContext.js");
894
+ const durableState = await loadDurableContext(process.cwd());
895
+ const composed = composeProjectContext({
896
+ mode: overrides.ragContext ? "zelari" : "council",
897
+ cwd: process.cwd(),
898
+ userMessage: effectiveText,
899
+ memoryHits: overrides.ragContext,
900
+ durableState: durableState || undefined,
901
+ historySnippet: formatHistoryForCouncil(4) || undefined,
902
+ includeLessons: true,
903
+ includeDurableState: false,
904
+ });
905
+ for (const w of composed.warnings) {
906
+ appendSystem(setMessages, w, Date.now());
907
+ }
908
+ councilCompose = {
909
+ workspaceContext: composed.workspaceContext,
910
+ ...(composed.ragContext ? { ragContext: composed.ragContext } : {}),
911
+ };
912
+ }
913
+ catch {
914
+ if (overrides.ragContext) {
915
+ councilCompose = {
916
+ workspaceContext: formatHistoryForCouncil(4) || "",
917
+ ragContext: overrides.ragContext,
918
+ };
919
+ }
920
+ }
766
921
  try {
767
922
  for await (const event of dispatchCouncil(effectiveText, {
768
923
  apiKey: envConfig.apiKey,
@@ -772,30 +927,20 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
772
927
  sessionId,
773
928
  tools: councilToolRegistry,
774
929
  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"),
930
+ workspaceContext: councilCompose.workspaceContext,
931
+ ...(councilCompose.ragContext
932
+ ? { ragContext: councilCompose.ragContext }
933
+ : {}),
791
934
  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 } : {}),
935
+ maxToolLoopIterations: councilMaxToolLoop,
936
+ ...(councilMaxToolLoopHard > 0
937
+ ? { maxToolLoopHardCap: councilMaxToolLoopHard }
938
+ : {}),
795
939
  runMode: phaseRunMode,
796
940
  ...(overrides.maxToolCallsChairman
797
941
  ? { maxToolCallsChairman: overrides.maxToolCallsChairman }
798
942
  : {}),
943
+ ...(overrides.skipSpecialists ? { skipSpecialists: true } : {}),
799
944
  onCouncilStatus: (message) => {
800
945
  appendSystem(setMessages, message, Date.now());
801
946
  },
@@ -811,6 +956,12 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
811
956
  question: req.question,
812
957
  choices,
813
958
  });
959
+ const choiceLines = choices
960
+ .map((c, i) => ` ${i + 1}. ${c}`)
961
+ .join("\n");
962
+ appendSystem(setMessages, `[council in attesa]\n${req.question}\n${choiceLines}` +
963
+ (req.context ? `\n_(${req.context})_` : "") +
964
+ "\n→ scegli dalla lista (il membro riprende dopo la risposta).", Date.now());
814
965
  let settled = false;
815
966
  const finish = (value) => {
816
967
  if (settled)
@@ -1083,6 +1234,48 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
1083
1234
  appendSystem(setMessages, `[completion] readyToCommit=false — ${n} blocking issue(s)${c.degraded ? " (degraded run)" : ""}`, Date.now());
1084
1235
  }
1085
1236
  }
1237
+ // Durable State Commit after verified council completion (Palmer).
1238
+ // Skip degraded / failed verification — those must not become HEAD.
1239
+ const verifyOk = hook.verification?.ran
1240
+ ? hook.verification.ok === true
1241
+ : false;
1242
+ const completionOk = hook.completion?.completion?.ok === true ||
1243
+ hook.completion?.completion?.readyToCommit === true;
1244
+ if (!degraded.degraded &&
1245
+ !councilAborted &&
1246
+ (verifyOk || completionOk) &&
1247
+ luciferWriteCount > 0) {
1248
+ try {
1249
+ const { tryStateCommit, discoveriesFromOutcome } = await import("../state/commitHelpers.js");
1250
+ const res = await tryStateCommit({
1251
+ projectRoot: process.cwd(),
1252
+ mode: "council",
1253
+ layer: `council:${councilRunMode ?? "run"}`,
1254
+ label: `council ${councilRunMode ?? "run"} verified`,
1255
+ sessionId,
1256
+ verification: {
1257
+ ok: true,
1258
+ ran: hook.verification?.ran ?? completionOk,
1259
+ },
1260
+ withCheckpoint: true,
1261
+ discoveries: discoveriesFromOutcome({
1262
+ stepId: `${sessionId}-${Date.now()}`,
1263
+ synthesis: chairmanSynthesisText,
1264
+ writeCount: luciferWriteCount,
1265
+ note: "Council implementation verified",
1266
+ }),
1267
+ });
1268
+ if (res.ok && res.meta?.id) {
1269
+ appendSystem(setMessages, `[state] commit ${res.meta.id}` +
1270
+ (res.checkpointId
1271
+ ? ` · checkpoint ${res.checkpointId}`
1272
+ : ""), Date.now());
1273
+ }
1274
+ }
1275
+ catch {
1276
+ // fail-open
1277
+ }
1278
+ }
1086
1279
  }
1087
1280
  catch {
1088
1281
  // Best-effort — never block on AGENTS.MD errors.
@@ -1171,11 +1364,12 @@ async function runZelariMissionInTui(userMessage, deps, emit) {
1171
1364
  projectRoot,
1172
1365
  memory,
1173
1366
  emit,
1174
- runSlice: async ({ userMessage: slicePrompt, runMode, ragContext }) => {
1367
+ runSlice: async ({ userMessage: slicePrompt, runMode, ragContext, implementerRetry, }) => {
1175
1368
  const r = await dispatchCouncilPromptImpl(slicePrompt, deps, {
1176
1369
  ragContext,
1177
1370
  runMode,
1178
1371
  maxToolCallsChairman: chairmanBudget,
1372
+ ...(implementerRetry ? { skipSpecialists: true } : {}),
1179
1373
  });
1180
1374
  return {
1181
1375
  completionOk: r.completionOk,