zelari-code 1.15.0 → 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.
- package/README.md +3 -1
- package/dist/cli/app.js +7 -1
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/components/StatusBar.js +5 -1
- package/dist/cli/components/StatusBar.js.map +1 -1
- package/dist/cli/councilDispatcher.js +3 -0
- package/dist/cli/councilDispatcher.js.map +1 -1
- package/dist/cli/hooks/chatStats.js +20 -2
- package/dist/cli/hooks/chatStats.js.map +1 -1
- package/dist/cli/hooks/conversationContext.js +5 -2
- package/dist/cli/hooks/conversationContext.js.map +1 -1
- package/dist/cli/hooks/historyCompaction.js +8 -1
- package/dist/cli/hooks/historyCompaction.js.map +1 -1
- package/dist/cli/hooks/useChatTurn.js +236 -60
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useSlashDispatch.js +42 -0
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/main.bundled.js +2385 -846
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +47 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mcp/mcpManager.js +11 -1
- package/dist/cli/mcp/mcpManager.js.map +1 -1
- package/dist/cli/mcp/mcpPresets.js +77 -0
- package/dist/cli/mcp/mcpPresets.js.map +1 -0
- package/dist/cli/memory/fileBackend.js +11 -2
- package/dist/cli/memory/fileBackend.js.map +1 -1
- package/dist/cli/runHeadless.js +61 -4
- package/dist/cli/runHeadless.js.map +1 -1
- package/dist/cli/slashCommands.js +54 -1
- package/dist/cli/slashCommands.js.map +1 -1
- package/dist/cli/slashHandlers/cache.js +28 -0
- package/dist/cli/slashHandlers/cache.js.map +1 -0
- package/dist/cli/slashHandlers/state.js +83 -0
- package/dist/cli/slashHandlers/state.js.map +1 -0
- package/dist/cli/state/commitHelpers.js +62 -0
- package/dist/cli/state/commitHelpers.js.map +1 -0
- package/dist/cli/state/fileStateStore.js +262 -0
- package/dist/cli/state/fileStateStore.js.map +1 -0
- package/dist/cli/state/promptCacheStats.js +56 -0
- package/dist/cli/state/promptCacheStats.js.map +1 -0
- package/dist/cli/state/restoreState.js +70 -0
- package/dist/cli/state/restoreState.js.map +1 -0
- package/dist/cli/toolRegistry.js +8 -0
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/cli/tools/askUser.js +71 -0
- package/dist/cli/tools/askUser.js.map +1 -0
- package/dist/cli/utils/doctor.js +19 -0
- package/dist/cli/utils/doctor.js.map +1 -1
- package/dist/cli/workspace/composeContext.js +210 -0
- package/dist/cli/workspace/composeContext.js.map +1 -0
- package/dist/cli/workspace/workspaceSummary.js +58 -31
- package/dist/cli/workspace/workspaceSummary.js.map +1 -1
- package/dist/cli/zelariMission.js +135 -18
- package/dist/cli/zelariMission.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
let
|
|
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 {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 (
|
|
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
|
-
|
|
232
|
-
? "-
|
|
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
|
-
|
|
294
|
+
].join("\n");
|
|
295
|
+
const volatileSessionBlock = [
|
|
296
|
+
planPhaseBlock,
|
|
297
|
+
hasPlan
|
|
250
298
|
? [
|
|
251
|
-
"",
|
|
252
299
|
"# Plan Tracking",
|
|
253
|
-
'- Plan tasks: when you START
|
|
254
|
-
]
|
|
255
|
-
:
|
|
256
|
-
]
|
|
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
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
const workspaceContext = [
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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,
|