zelari-code 1.7.1 → 1.8.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 (36) hide show
  1. package/dist/cli/app.js +18 -4
  2. package/dist/cli/app.js.map +1 -1
  3. package/dist/cli/browser/driver.js +58 -10
  4. package/dist/cli/browser/driver.js.map +1 -1
  5. package/dist/cli/browser/tools.js +4 -1
  6. package/dist/cli/browser/tools.js.map +1 -1
  7. package/dist/cli/budget/tokenBudget.js +92 -0
  8. package/dist/cli/budget/tokenBudget.js.map +1 -0
  9. package/dist/cli/components/PluginGate.js +24 -1
  10. package/dist/cli/components/PluginGate.js.map +1 -1
  11. package/dist/cli/components/Sidebar.js +7 -31
  12. package/dist/cli/components/Sidebar.js.map +1 -1
  13. package/dist/cli/components/StatusBar.js +18 -3
  14. package/dist/cli/components/StatusBar.js.map +1 -1
  15. package/dist/cli/councilDispatcher.js +3 -0
  16. package/dist/cli/councilDispatcher.js.map +1 -1
  17. package/dist/cli/hooks/conversationContext.js +136 -0
  18. package/dist/cli/hooks/conversationContext.js.map +1 -0
  19. package/dist/cli/hooks/useChatTurn.js +156 -56
  20. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  21. package/dist/cli/hooks/useSlashDispatch.js +55 -0
  22. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  23. package/dist/cli/main.bundled.js +1043 -393
  24. package/dist/cli/main.bundled.js.map +4 -4
  25. package/dist/cli/phase.js +52 -0
  26. package/dist/cli/phase.js.map +1 -0
  27. package/dist/cli/phaseState.js +12 -0
  28. package/dist/cli/phaseState.js.map +1 -0
  29. package/dist/cli/plugins/registry.js +90 -37
  30. package/dist/cli/plugins/registry.js.map +1 -1
  31. package/dist/cli/slashCommands.js +22 -0
  32. package/dist/cli/slashCommands.js.map +1 -1
  33. package/dist/cli/slashHandlers/provider.js.map +1 -1
  34. package/dist/cli/toolRegistry.js +3 -2
  35. package/dist/cli/toolRegistry.js.map +1 -1
  36. package/package.json +2 -2
@@ -0,0 +1,136 @@
1
+ /**
2
+ * conversationContext — shared rolling provider history for all dispatch modes
3
+ * (agent / council / zelari).
4
+ *
5
+ * v1.6.0 fixed single-agent context loss with an in-hook historyRef. That left
6
+ * council/zelari stateless across turns, and /clear|/new never reset history —
7
+ * so the model could still "forget" answers or leak prior sessions.
8
+ *
9
+ * This module is the single source of truth for provider-side AgentMessage[]
10
+ * history. Hooks read/write it; slash handlers call clear() on /clear|/new.
11
+ *
12
+ * @since v1.8.0 (PR-A context unification)
13
+ */
14
+ import { compactHistory } from './historyCompaction.js';
15
+ let history = [];
16
+ let lastClarification = null;
17
+ /** Current rolling history (read-only view — callers must not mutate). */
18
+ export function getHistory() {
19
+ return history;
20
+ }
21
+ /** Replace history after compaction / hydrate. */
22
+ export function setHistory(messages) {
23
+ history = [...messages];
24
+ }
25
+ /** Compact in place using the same rules as the agent loop. */
26
+ export function compactInPlace() {
27
+ history = compactHistory(history);
28
+ }
29
+ /** Append messages (e.g. this turn's assistant+tool tail). */
30
+ export function appendMessages(msgs) {
31
+ if (msgs.length === 0)
32
+ return;
33
+ history = history.concat(msgs);
34
+ }
35
+ /** Drop everything ( /clear, /new ). */
36
+ export function clearHistory() {
37
+ history = [];
38
+ lastClarification = null;
39
+ }
40
+ /** Serialize for session sidecar / tests. */
41
+ export function serializeHistory() {
42
+ return [...history];
43
+ }
44
+ /** Hydrate from session restore. */
45
+ export function hydrateHistory(messages) {
46
+ history = [...messages];
47
+ }
48
+ export function getLastClarification() {
49
+ return lastClarification;
50
+ }
51
+ export function setLastClarification(c) {
52
+ lastClarification = c
53
+ ? { question: c.question, choices: c.choices, at: Date.now() }
54
+ : null;
55
+ }
56
+ /**
57
+ * If the user sends a short reply that likely answers the last clarifying
58
+ * question, return a rewritten user message that re-anchors the question so
59
+ * the model cannot treat "full" / "2" / "sì" as a brand-new request.
60
+ *
61
+ * Returns null when no rewrite is needed (long free-form message, no prior
62
+ * question, or history already carries enough context).
63
+ */
64
+ export function maybeAnchorShortAnswer(userText) {
65
+ const clar = lastClarification;
66
+ if (!clar)
67
+ return null;
68
+ const trimmed = userText.trim();
69
+ if (!trimmed)
70
+ return null;
71
+ // Long free-form answers already carry intent — don't wrap.
72
+ if (trimmed.length > 80 || trimmed.includes('\n'))
73
+ return null;
74
+ const lower = trimmed.toLowerCase();
75
+ const choices = clar.choices;
76
+ const matched = choices.find((c) => c.toLowerCase() === lower) ??
77
+ choices.find((c) => c.toLowerCase().startsWith(lower)) ??
78
+ choices.find((c) => lower.startsWith(c.toLowerCase().slice(0, Math.min(4, c.length))));
79
+ // Also treat numeric picks ("1", "2") as choice indices (1-based).
80
+ let choiceLabel = matched ?? null;
81
+ if (!choiceLabel && /^\d{1,2}$/.test(trimmed)) {
82
+ const idx = Number.parseInt(trimmed, 10) - 1;
83
+ if (idx >= 0 && idx < choices.length)
84
+ choiceLabel = choices[idx] ?? null;
85
+ }
86
+ // Very short replies without a choice match still get anchored if ≤ 24 chars
87
+ // (e.g. "sì", "ok", "la seconda") — the model needs the prior question.
88
+ if (!choiceLabel && trimmed.length > 24)
89
+ return null;
90
+ const picked = choiceLabel ?? trimmed;
91
+ return (`The user is answering your previous clarifying question.\n` +
92
+ `Question: ${clar.question}\n` +
93
+ `Choices were: ${choices.join(' | ')}\n` +
94
+ `User's answer: ${picked}\n` +
95
+ `Proceed using this answer; do not re-ask the same question unless the answer is still ambiguous.`);
96
+ }
97
+ /**
98
+ * Build a compact text block of recent conversation for council/zelari paths
99
+ * that do not feed full AgentMessage[] into every member (token control).
100
+ * Includes the last few user/assistant turns only.
101
+ */
102
+ export function formatHistoryForCouncil(maxTurns = 4) {
103
+ if (history.length === 0)
104
+ return '';
105
+ const lines = [];
106
+ // Walk from the end; collect up to maxTurns user+assistant pairs.
107
+ let turns = 0;
108
+ const chunk = [];
109
+ for (let i = history.length - 1; i >= 0 && turns < maxTurns; i--) {
110
+ const m = history[i];
111
+ if (m.role === 'user') {
112
+ chunk.push(`User: ${truncate(m.content, 400)}`);
113
+ turns += 1;
114
+ }
115
+ else if (m.role === 'assistant' && m.content.trim()) {
116
+ chunk.push(`Assistant: ${truncate(m.content, 600)}`);
117
+ }
118
+ }
119
+ if (chunk.length === 0)
120
+ return '';
121
+ lines.push('## Prior conversation (rolling context)');
122
+ lines.push(...chunk.reverse());
123
+ return lines.join('\n');
124
+ }
125
+ function truncate(s, max) {
126
+ const t = s.replace(/\s+/g, ' ').trim();
127
+ if (t.length <= max)
128
+ return t;
129
+ return `${t.slice(0, max - 1)}…`;
130
+ }
131
+ /** Test-only: reset module state. */
132
+ export function _resetConversationContextForTests() {
133
+ history = [];
134
+ lastClarification = null;
135
+ }
136
+ //# sourceMappingURL=conversationContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversationContext.js","sourceRoot":"","sources":["../../../src/cli/hooks/conversationContext.ts"],"names":[],"mappings":"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"}
@@ -13,27 +13,27 @@ import { buildSystemPrompt, getAllTools, SINGLE_AGENT_IDENTITY_MODULE, buildLang
13
13
  import { parseClarificationRequest, cleanAgentContent, } from "@zelari/core";
14
14
  import { appendOrExtendStreamingAssistant, appendSystem, appendToolStart, finalizeStreamingAssistant, updateToolMessageEnd, } from "./messageHelpers.js";
15
15
  import { setStreaming, finalizeStreaming, startTool, completeTool, } from "./chatState.js";
16
- import { compactHistory } from "./historyCompaction.js";
16
+ import { getHistory, compactInPlace, appendMessages, clearHistory, setLastClarification, maybeAnchorShortAnswer, formatHistoryForCouncil, setHistory, } from "./conversationContext.js";
17
17
  import { computeSessionStatsDelta } from "./chatStats.js";
18
18
  import { envNumber } from "../utils/envNumber.js";
19
+ import { getPhase } from "../phaseState.js";
20
+ import { describePhase } from "../phase.js";
21
+ import { applyBudgetPolicy } from "../budget/tokenBudget.js";
19
22
  export function useChatTurn(params) {
20
23
  const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setSessionActive, setSessionStats, setLive, liveRef, setPicker, } = params;
21
24
  const harnessRef = useRef(null);
22
25
  const [queueCount, setQueueCount] = useState(0);
23
- // v1.6.0: rolling conversation history. The single-agent loop was
24
- // stateless across turns each dispatchPrompt rebuilt
25
- // messages:[{system},{user}] from scratch, so the model never saw its
26
- // own prior question when the user answered with a short reply ("full",
27
- // "sì", "la seconda"). This ref carries prior turns forward: the seed
28
- // for turn N is [system, ...history, user_N], and after the run we
29
- // snapshot the assistant+tool tail the harness accumulated and append
30
- // it here for turn N+1. Capped by compactHistory() (ZELARI_HISTORY_TURNS).
31
- const historyRef = useRef([]);
26
+ // v1.8.0: rolling history lives in conversationContext (shared by agent,
27
+ // council, zelari) so /clear|/new can reset it and short answers bind
28
+ // across all modes. Seed for turn N is [system, ...history, user_N].
32
29
  // v0.7.0: when the live region is wired, streaming + tool events route
33
30
  // there; otherwise we fall back to the v0.6 single-array behavior so the
34
31
  // existing unit tests (which pass only setMessages/commitStreaming) keep
35
32
  // asserting on `messages` directly.
36
33
  const useLiveModel = !!(setLive && liveRef);
34
+ const clearConversationHistory = useCallback(() => {
35
+ clearHistory();
36
+ }, []);
37
37
  const dispatchPrompt = useCallback(async (userText, opts) => {
38
38
  // v0.4.3 audit fix: provider resolution + harness construction now
39
39
  // live INSIDE the try block. Previously, throws from providerFromEnv,
@@ -54,13 +54,19 @@ export function useChatTurn(params) {
54
54
  // with a partial assistant tail).
55
55
  let turnSucceeded = false;
56
56
  try {
57
- // v1.6.0: trim rolling history before seeding the next turn. No-op
58
- // (returns the same ref) under the cap; replaces the array when it
59
- // exceeds ZELARI_HISTORY_TURNS. When the env var is 0 the cap
60
- // resolves to [] and the loop degrades to the pre-1.6 stateless
61
- // [system, user] behavior.
62
- historyRef.current = compactHistory(historyRef.current);
63
- historySeedLen = historyRef.current.length;
57
+ // v1.8.0: budget-aware compact (phase plan/build + occupancy thresholds).
58
+ compactInPlace();
59
+ const budget = applyBudgetPolicy(getHistory(), getPhase());
60
+ setHistory(budget.history);
61
+ for (const w of budget.warnings) {
62
+ appendSystem(setMessages, w, Date.now());
63
+ }
64
+ historySeedLen = getHistory().length;
65
+ // Short-answer anchor: if the user is replying to a ---QUESTION---,
66
+ // rewrite the user message so the model cannot treat "full"/"2" as
67
+ // a brand-new request even if compaction dropped the prior turn.
68
+ const anchored = maybeAnchorShortAnswer(userText);
69
+ const effectiveUserText = anchored ?? userText;
64
70
  envConfig = await providerFromEnv();
65
71
  if (!envConfig) {
66
72
  // Name the ACTIVE provider — the old hardcoded "OPENAI_API_KEY not
@@ -71,7 +77,10 @@ export function useChatTurn(params) {
71
77
  return;
72
78
  }
73
79
  setBusy(true);
74
- const { registry: toolRegistry } = createBuiltinToolRegistry();
80
+ const workPhase = getPhase();
81
+ const { registry: toolRegistry } = createBuiltinToolRegistry({
82
+ planMode: workPhase === "plan",
83
+ });
75
84
  const baseProviderStream = openaiCompatibleProvider(envConfig);
76
85
  const failoverResolution = await resolveFailoverStream({
77
86
  failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
@@ -200,12 +209,30 @@ export function useChatTurn(params) {
200
209
  // SINGLE_AGENT_IDENTITY_MODULE overrides the council-flavored
201
210
  // 'base-identity' module so the persona is "Zelari Code in the terminal",
202
211
  // not "member of an AI Council".
212
+ const planPhaseBlock = workPhase === "plan"
213
+ ? [
214
+ "",
215
+ "# Work Phase: PLAN",
216
+ "You are in PLAN mode. Explore and design only.",
217
+ "- Do NOT implement production code or run destructive shell commands.",
218
+ "- write_file / edit_file / bash / apply_diff are unavailable.",
219
+ "- Produce a clear plan, ask clarifying questions (---QUESTION---), use workspace plan tools when relevant.",
220
+ "- When the plan is ready, tell the user to run /build to implement.",
221
+ ].join("\n")
222
+ : workPhase === "build" && planSummary
223
+ ? [
224
+ "",
225
+ "# Work Phase: BUILD",
226
+ "Implement the approved plan. Prefer acting over describing. Update plan task statuses as you go.",
227
+ ].join("\n")
228
+ : "";
203
229
  const shellContextBlock = [
204
230
  "# Platform & Shell",
205
231
  `platform: ${process.platform}`,
206
232
  `shell: ${resolvedShell.via}`,
207
233
  shellGuidance,
208
234
  nonInteractiveGuidance,
235
+ planPhaseBlock,
209
236
  "",
210
237
  "# Working Directory",
211
238
  `You are running in: ${cwd}`,
@@ -284,23 +311,19 @@ export function useChatTurn(params) {
284
311
  default: 25,
285
312
  min: 1,
286
313
  });
287
- // v1.5.2: tool-loop iteration cap (observe reason act cycles per
288
- // run). Core default is 90 (raised from 30). Overridable for very
289
- // large multi-file tasks or to tighten on flaky providers.
290
- const maxToolLoopIterations = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, {
291
- default: 90,
292
- min: 1,
293
- });
314
+ // v1.5.2 / v1.8.0: tool-loop cap budget policy may lower under
315
+ // context pressure; env still wins as the ceiling via applyBudgetPolicy.
316
+ const maxToolLoopIterations = budget.maxToolLoopIterations;
294
317
  const harness = new AgentHarness({
295
318
  model: envConfig.model,
296
319
  provider: "openai-compatible",
297
320
  messages: [
298
321
  { role: "system", content: systemPrompt },
299
- // v1.6.0: seed prior turns so the model sees its own last
300
- // question when the user answers briefly. historyRef.current
301
- // is compacted above (possibly empty if ZELARI_HISTORY_TURNS=0).
302
- ...historyRef.current,
303
- { role: "user", content: userText },
322
+ // v1.8.0: shared rolling history (agent/council/zelari) so short
323
+ // answers bind to prior ---QUESTION--- blocks. Possibly empty
324
+ // when ZELARI_HISTORY_TURNS=0.
325
+ ...getHistory(),
326
+ { role: "user", content: effectiveUserText },
304
327
  ],
305
328
  tools: toolRegistry.toOpenAITools().map((t) => ({
306
329
  name: t.function.name,
@@ -455,24 +478,24 @@ export function useChatTurn(params) {
455
478
  const all = h.getMessages();
456
479
  const seedLen = 1 /*system*/ + historySeedLen + 1 /*user*/;
457
480
  if (all.length > seedLen) {
458
- historyRef.current = historyRef.current.concat(all.slice(seedLen));
481
+ appendMessages(all.slice(seedLen));
459
482
  }
460
483
  }
461
484
  }
462
485
  catch {
463
486
  // Non-fatal: a snapshot failure must never break the turn.
464
487
  }
465
- // v1.6.0: if the assistant ended its turn by posing a clarifying
466
- // question (---QUESTION--- block), parse it and open the picker so
467
- // the user can pick from the offered choices. Rolling history
468
- // (above) already guarantees the model sees its own question next
469
- // turn, so the binding works whether the user picks or types. The
470
- // picker is purely ergonomic. Skipped when setPicker is unset
471
- // (tests) or the run failed.
472
- if (turnSucceeded && setPicker && assistantContent) {
488
+ // v1.6.0/v1.8.0: clarifying-question picker + lastClarification for
489
+ // short-answer anchoring. Rolling history already binds answers;
490
+ // setLastClarification covers compaction edge cases.
491
+ if (turnSucceeded && assistantContent) {
473
492
  try {
474
493
  const clar = parseClarificationRequest(assistantContent);
475
494
  if (clar && clar.choices && clar.choices.length >= 2) {
495
+ setLastClarification({
496
+ question: clar.question,
497
+ choices: clar.choices,
498
+ });
476
499
  // Strip the raw ---QUESTION--- block from the finalized
477
500
  // assistant message so the display shows prose, not JSON.
478
501
  const cleaned = cleanAgentContent(assistantContent);
@@ -488,14 +511,20 @@ export function useChatTurn(params) {
488
511
  return next;
489
512
  });
490
513
  }
491
- setPicker({
492
- kind: "clarification",
493
- title: clar.question,
494
- items: clar.choices.map((c) => ({ value: c, label: c })),
495
- onAnswer: (value) => {
496
- void dispatchPrompt(value);
497
- },
498
- });
514
+ if (setPicker) {
515
+ setPicker({
516
+ kind: "clarification",
517
+ title: clar.question,
518
+ items: clar.choices.map((c) => ({ value: c, label: c })),
519
+ onAnswer: (value) => {
520
+ void dispatchPrompt(value);
521
+ },
522
+ });
523
+ }
524
+ }
525
+ else {
526
+ // Free-form answer path next turn — clear stale question.
527
+ setLastClarification(null);
499
528
  }
500
529
  }
501
530
  catch {
@@ -543,6 +572,7 @@ export function useChatTurn(params) {
543
572
  setQueueCount,
544
573
  setLive,
545
574
  liveRef,
575
+ setPicker,
546
576
  });
547
577
  }, [
548
578
  sessionId,
@@ -554,6 +584,7 @@ export function useChatTurn(params) {
554
584
  setQueueCount,
555
585
  setLive,
556
586
  liveRef,
587
+ setPicker,
557
588
  ]);
558
589
  const pendingZelariRef = useRef(null);
559
590
  const dispatchZelariPrompt = useCallback(async (text) => {
@@ -586,10 +617,11 @@ export function useChatTurn(params) {
586
617
  harnessRef,
587
618
  queueCount,
588
619
  setQueueCount,
620
+ clearConversationHistory,
589
621
  };
590
622
  }
591
623
  async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
592
- const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setLive, liveRef, } = deps;
624
+ const { sessionId, writerRef, setMessages, commitStreaming, flushStreaming, setBusy, setLive, liveRef, setPicker, } = deps;
593
625
  const useLiveModel = !!(setLive && liveRef);
594
626
  const envConfig = await providerFromEnv();
595
627
  if (!envConfig) {
@@ -599,6 +631,16 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
599
631
  return { completionOk: false, ran: false };
600
632
  }
601
633
  setBusy(true);
634
+ // v1.8.0: compact shared history + budget + short-answer anchor.
635
+ compactInPlace();
636
+ const councilBudget = applyBudgetPolicy(getHistory(), getPhase());
637
+ setHistory(councilBudget.history);
638
+ for (const w of councilBudget.warnings) {
639
+ appendSystem(setMessages, w, Date.now());
640
+ }
641
+ const anchored = maybeAnchorShortAnswer(text);
642
+ const effectiveText = anchored ?? text;
643
+ appendSystem(setMessages, `[phase] ${describePhase(getPhase())}`, Date.now());
602
644
  // Import dynamically to avoid a circular dep at module-load time.
603
645
  const { dispatchCouncil } = await import("../councilDispatcher.js");
604
646
  const { createWorkspaceContext, createWorkspaceStubs } = await import("../workspace/stubs.js");
@@ -608,14 +650,23 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
608
650
  const { buildWorkspaceSummary, buildPlanSummary } = await import("../workspace/workspaceSummary.js");
609
651
  const { buildLessonsSummary } = await import("../workspace/buildLessonsSummary.js");
610
652
  const { FeedbackStore } = await import("../councilFeedback.js");
611
- const { registry: councilToolRegistry } = createBuiltinToolRegistry();
653
+ const workPhase = getPhase();
654
+ const { registry: councilToolRegistry } = createBuiltinToolRegistry({
655
+ planMode: workPhase === "plan",
656
+ });
612
657
  const workspaceCtx = createWorkspaceContext();
613
658
  const workspaceReg = createWorkspaceToolRegistry(workspaceCtx);
614
659
  for (const name of workspaceReg.list()) {
615
660
  const td = workspaceReg.get(name);
616
- if (td)
617
- councilToolRegistry.register(td);
661
+ if (!td)
662
+ continue;
663
+ // Plan phase: keep workspace plan/doc tools (createPlan, …); skip any
664
+ // that are pure project-file mutators if ever added.
665
+ councilToolRegistry.register(td);
618
666
  }
667
+ // Force council design-phase when UI phase is plan (and vice-versa for build).
668
+ const phaseRunMode = overrides.runMode ??
669
+ (workPhase === "plan" ? "design-phase" : "implementation");
619
670
  // v0.7.5: MCP tools for the council too (same lazy singleton as the
620
671
  // single-agent path — zero extra spawns).
621
672
  try {
@@ -666,7 +717,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
666
717
  let sliceDegraded = false;
667
718
  const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
668
719
  try {
669
- for await (const event of dispatchCouncil(text, {
720
+ for await (const event of dispatchCouncil(effectiveText, {
670
721
  apiKey: envConfig.apiKey,
671
722
  model: envConfig.model,
672
723
  provider: "openai-compatible",
@@ -680,10 +731,13 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
680
731
  // on and projected their identity onto the task.
681
732
  // v0.7.3: append the existing plan (if any) so a follow-up /council
682
733
  // continues it instead of re-planning from scratch.
734
+ // v1.8.0: rolling conversation context so short answers bind across
735
+ // council turns (same history store as the single-agent path).
683
736
  workspaceContext: [
684
737
  buildWorkspaceSummary(process.cwd()),
685
- buildPlanSummary(process.cwd(), { userMessage: text }),
686
- buildLessonsSummary(process.cwd(), text),
738
+ buildPlanSummary(process.cwd(), { userMessage: effectiveText }),
739
+ buildLessonsSummary(process.cwd(), effectiveText),
740
+ formatHistoryForCouncil(4),
687
741
  ]
688
742
  .filter(Boolean)
689
743
  .join("\n\n"),
@@ -691,13 +745,42 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
691
745
  // v1.0: Zelari-mode per-slice overrides (memory RAG, forced run mode,
692
746
  // raised chairman budget). No-ops for a normal /council run.
693
747
  ...(overrides.ragContext ? { ragContext: overrides.ragContext } : {}),
694
- ...(overrides.runMode ? { runMode: overrides.runMode } : {}),
748
+ runMode: phaseRunMode,
695
749
  ...(overrides.maxToolCallsChairman
696
750
  ? { maxToolCallsChairman: overrides.maxToolCallsChairman }
697
751
  : {}),
698
752
  onCouncilStatus: (message) => {
699
753
  appendSystem(setMessages, message, Date.now());
700
754
  },
755
+ // v1.8.0: pause council when a member asks a structured question.
756
+ onClarification: setPicker
757
+ ? (req) => new Promise((resolve) => {
758
+ const choices = req.choices ?? [];
759
+ if (choices.length < 2) {
760
+ resolve(null);
761
+ return;
762
+ }
763
+ setLastClarification({
764
+ question: req.question,
765
+ choices,
766
+ });
767
+ let settled = false;
768
+ const finish = (value) => {
769
+ if (settled)
770
+ return;
771
+ settled = true;
772
+ setPicker(null);
773
+ resolve(value);
774
+ };
775
+ setPicker({
776
+ kind: "clarification",
777
+ title: req.question,
778
+ items: choices.map((c) => ({ value: c, label: c })),
779
+ onAnswer: (value) => finish(value),
780
+ onCancel: () => finish(null),
781
+ });
782
+ })
783
+ : undefined,
701
784
  })) {
702
785
  if (councilAborted) {
703
786
  // Drain remaining events silently after the abort decision.
@@ -857,6 +940,23 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
857
940
  finalizeStreaming(setMessages, setLive);
858
941
  else
859
942
  finalizeStreamingAssistant(setMessages);
943
+ // v1.8.0: fold this council turn into shared rolling history so the next
944
+ // agent/council/zelari turn sees user + synthesis (short answers bind).
945
+ if (membersCompleted > 0 || chairmanProducedOutput) {
946
+ try {
947
+ appendMessages([
948
+ { role: "user", content: effectiveText },
949
+ {
950
+ role: "assistant",
951
+ content: chairmanSynthesisText.trim() ||
952
+ "[council completed without chairman synthesis text]",
953
+ },
954
+ ]);
955
+ }
956
+ catch {
957
+ // Non-fatal.
958
+ }
959
+ }
860
960
  // v0.7.1 (A3): only auto-write AGENTS.MD when the council actually produced
861
961
  // output. Running the hook after an all-error run (e.g. the HTTP 400 from
862
962
  // A1) dirtied the working tree with sections rewritten from nothing.
@@ -878,7 +978,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
878
978
  }
879
979
  const hook = await runPostCouncilHook(workspaceCtx, {
880
980
  runMode: councilRunMode,
881
- userMessage: text,
981
+ userMessage: effectiveText,
882
982
  synthesisText: chairmanSynthesisText || undefined,
883
983
  degradedRun: degraded.degraded,
884
984
  degradedReasons: degraded.reasons,