zelari-code 1.13.0 → 1.14.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.
@@ -18345,7 +18345,9 @@ Earlier members' outputs are shared context. Build on them; do not re-derive or
18345
18345
 
18346
18346
  - Be concise and structured. Prefer short markdown sections and bullets over walls of text.
18347
18347
  - Be proactive but not reckless: if a single missing fact would change the design, ask one focused question; otherwise make a documented assumption and proceed.
18348
- - Prefer action over description when the user wants code, fixes, or repo changes \u2014 use tools.
18348
+ - Prefer action over description when the user wants code, fixes, or repo changes \u2014 use tools (write_file/edit_file/bash), not prose-only plans.
18349
+ - When the user confirms a plan ("procedi", "s\xEC", "implementa"), the prior plan is work TO DO on disk. Reading alone is incomplete.
18350
+ - Never claim work is "already implemented" without verifying the real files (and writing if gaps remain).
18349
18351
  - Think step by step internally; surface conclusions and a brief rationale, not a full chain of thought.`
18350
18352
  };
18351
18353
  BEHAVIOR_COUNCIL = {
@@ -18416,11 +18418,12 @@ Earlier members' outputs are shared context. Build on them; do not re-derive or
18416
18418
  content: `# Coding Practices
18417
18419
 
18418
18420
  - **Read before edit**: open relevant files (and nearby callers/tests) before changing code.
18421
+ - **Then write**: if the task is to implement, follow reads with write_file/edit_file in the same turn. Do not end after exploration.
18419
18422
  - **Minimal diffs**: change only what the task requires; match existing style and patterns.
18420
18423
  - **Don't invent**: no fake APIs, deps, or config keys \u2014 discover from the tree or package manifests.
18421
18424
  - **Verify**: after non-trivial edits, run the project's tests/typecheck/build when available; fix failures you introduced.
18422
18425
  - **Use project scripts**: prefer package.json / Makefile / existing tooling over ad-hoc commands.
18423
- - **Finish**: summarize what changed and how to verify.`
18426
+ - **Finish**: list the paths you wrote/edited and how to verify. If you wrote nothing, say so honestly \u2014 do not invent a done report.`
18424
18427
  };
18425
18428
  CLARIFICATION_PROTOCOL_MODULE = {
18426
18429
  type: "custom",
@@ -26988,14 +26991,18 @@ var conversationContext_exports = {};
26988
26991
  __export(conversationContext_exports, {
26989
26992
  _resetConversationContextForTests: () => _resetConversationContextForTests,
26990
26993
  appendMessages: () => appendMessages,
26994
+ buildAgentUserWithHistory: () => buildAgentUserWithHistory,
26995
+ buildContinueUserMessage: () => buildContinueUserMessage,
26991
26996
  buildCouncilTaskWithHistory: () => buildCouncilTaskWithHistory,
26992
26997
  clearHistory: () => clearHistory,
26993
26998
  compactInPlace: () => compactInPlace,
26999
+ expectsDiskImplementation: () => expectsDiskImplementation,
26994
27000
  formatHistoryForCouncil: () => formatHistoryForCouncil,
26995
27001
  formatHistoryMessages: () => formatHistoryMessages,
26996
27002
  getHistory: () => getHistory,
26997
27003
  getLastClarification: () => getLastClarification,
26998
27004
  hydrateHistory: () => hydrateHistory,
27005
+ isShortContinueReply: () => isShortContinueReply,
26999
27006
  maybeAnchorShortAnswer: () => maybeAnchorShortAnswer,
27000
27007
  serializeHistory: () => serializeHistory,
27001
27008
  setHistory: () => setHistory,
@@ -27078,22 +27085,67 @@ ${body.slice(body.length - maxTotalChars)}`;
27078
27085
  }
27079
27086
  return body;
27080
27087
  }
27081
- function buildCouncilTaskWithHistory(task, prior) {
27082
- const messages = prior ?? [];
27088
+ function isShortContinueReply(task) {
27083
27089
  const trimmed = task.trim();
27084
- let userPart = maybeAnchorShortAnswer(task) ?? task;
27085
- if (messages.length > 0 && (SHORT_CONTINUE.test(trimmed) || trimmed.length <= 40 && !trimmed.includes("\n"))) {
27086
- const lastAsst = [...messages].reverse().find((m) => m.role === "assistant" && m.content.trim());
27087
- if (lastAsst && SHORT_CONTINUE.test(trimmed)) {
27088
- userPart = `The user says "${trimmed}" \u2014 continue from the prior conversation. Do NOT restart from zero, do NOT re-ask for the overall goal, and do NOT ignore the prior plan/risks/decisions.
27090
+ if (!trimmed) return false;
27091
+ if (SHORT_CONTINUE.test(trimmed)) return true;
27092
+ if (trimmed.length <= 80 && !trimmed.includes("\n") && SHORT_CONTINUE_LOOSE.test(trimmed)) {
27093
+ return true;
27094
+ }
27095
+ return false;
27096
+ }
27097
+ function buildContinueUserMessage(task, prior, opts) {
27098
+ if (!isShortContinueReply(task) || prior.length === 0) return null;
27099
+ const lastAsst = [...prior].reverse().find((m) => m.role === "assistant" && (m.content ?? "").trim());
27100
+ if (!lastAsst) return null;
27101
+ const max = opts?.maxPriorChars ?? 8e3;
27102
+ const trimmed = task.trim();
27103
+ return `The user says "${trimmed}" \u2014 this is a CONTINUATION of an existing multi-turn session (the Desktop phase may have switched plan\u2194build; mode may have changed). This is NOT a new conversation and you DO have prior context below.
27104
+
27105
+ ## CRITICAL \u2014 plan text \u2260 done on disk
27106
+ The prior assistant output is a PLAN / SPEC / PROPOSAL (or analysis). It is NOT proof that project files already contain those changes.
27107
+ The user CONFIRMED the plan and wants you to IMPLEMENT it ON DISK NOW.
27108
+ - You MUST use write_file and/or edit_file (and bash when needed) to apply every planned change.
27109
+ - Reading files alone is incomplete. Do not stop after read_file/list_files/grep.
27110
+ - Do NOT claim "already implemented" / "tutto fatto" unless you verified the changes exist on disk in THIS turn (read after your own successful writes).
27111
+ - Do NOT restart from zero or re-ask for the overall goal.
27089
27112
 
27090
- ## Prior assistant output (authoritative context)
27091
- ${truncate(lastAsst.content, 4500)}
27113
+ ## Prior assistant output (plan to implement \u2014 authoritative)
27114
+ ${truncate(lastAsst.content, max)}
27092
27115
 
27093
27116
  ## Instruction
27094
- Proceed with the next concrete steps implied by that context (implementation when the work phase is build; otherwise the next planned actions).`;
27117
+ Implement the plan on disk now with mutating tools, then briefly list the files you wrote/edited.`;
27118
+ }
27119
+ function expectsDiskImplementation(task, phase2, prior) {
27120
+ if ((phase2 ?? "build") === "plan") return false;
27121
+ const trimmed = task.trim();
27122
+ if (!trimmed) return false;
27123
+ if (isShortContinueReply(trimmed)) return true;
27124
+ if (/\b(implement|implementa|scrivi|scriviamo|applica|modifica|fix|write|edit|crea|aggiungi|aggiorna|apply|patch)\b/i.test(
27125
+ trimmed
27126
+ )) {
27127
+ return true;
27128
+ }
27129
+ if (prior && prior.length > 0 && isShortContinueReply(trimmed)) {
27130
+ const lastAsst = [...prior].reverse().find((m) => m.role === "assistant" && (m.content ?? "").trim());
27131
+ if (lastAsst && /\b(se confermi|passo alla|scriv|implement|on disk|write_file|modifiche proposte|riepilogo delle modifiche)\b/i.test(
27132
+ lastAsst.content
27133
+ )) {
27134
+ return true;
27095
27135
  }
27096
27136
  }
27137
+ return false;
27138
+ }
27139
+ function buildCouncilTaskWithHistory(task, prior) {
27140
+ const messages = prior ?? [];
27141
+ const trimmed = task.trim();
27142
+ let userPart = maybeAnchorShortAnswer(task) ?? task;
27143
+ const continued = buildContinueUserMessage(trimmed, messages, {
27144
+ maxPriorChars: 4500
27145
+ });
27146
+ if (continued) {
27147
+ userPart = continued;
27148
+ }
27097
27149
  const block = formatHistoryMessages(messages, 6, 12e3);
27098
27150
  if (!block) return userPart;
27099
27151
  if (userPart.includes("Prior assistant output")) {
@@ -27104,6 +27156,12 @@ Proceed with the next concrete steps implied by that context (implementation whe
27104
27156
  ## Current user request
27105
27157
  ${userPart}`;
27106
27158
  }
27159
+ function buildAgentUserWithHistory(task, prior) {
27160
+ const messages = prior ?? [];
27161
+ const anchored = maybeAnchorShortAnswer(task);
27162
+ if (anchored) return anchored;
27163
+ return buildContinueUserMessage(task, messages, { maxPriorChars: 8e3 }) ?? task;
27164
+ }
27107
27165
  function truncate(s, max) {
27108
27166
  const t = s.replace(/\s+/g, " ").trim();
27109
27167
  if (t.length <= max) return t;
@@ -27113,14 +27171,15 @@ function _resetConversationContextForTests() {
27113
27171
  history = [];
27114
27172
  lastClarification = null;
27115
27173
  }
27116
- var history, lastClarification, SHORT_CONTINUE;
27174
+ var history, lastClarification, SHORT_CONTINUE, SHORT_CONTINUE_LOOSE;
27117
27175
  var init_conversationContext = __esm({
27118
27176
  "src/cli/hooks/conversationContext.ts"() {
27119
27177
  "use strict";
27120
27178
  init_historyCompaction();
27121
27179
  history = [];
27122
27180
  lastClarification = null;
27123
- SHORT_CONTINUE = /^(procedi|continua|continue|go\s*ahead|go|ok|okay|sì|si|yes|vai|avanti|next|proceed)$/i;
27181
+ SHORT_CONTINUE = /^(procedi|continua|continue|go\s*ahead|go|ok|okay|sì|si|yes|vai|avanti|next|proceed|conferma|confermo|applica|fai|scrivi|esegui|implementa|vai pure|fai pure|ok procedi|sì procedi|si procedi)$/i;
27182
+ SHORT_CONTINUE_LOOSE = /\b(procedi|continua|continue|conferma|confermo|applica|implementa|scriv[ia]|esegui|vai pure|fai pure|go ahead|proceed)\b/i;
27124
27183
  }
27125
27184
  });
27126
27185
 
@@ -35293,11 +35352,16 @@ function useChatTurn(params) {
35293
35352
  "- write_file / edit_file / bash / apply_diff are unavailable.",
35294
35353
  "- Produce a clear plan, ask clarifying questions (---QUESTION---), use workspace plan tools when relevant.",
35295
35354
  "- When the plan is ready, tell the user to run /build to implement."
35296
- ].join("\n") : workPhase === "build" && planSummary ? [
35355
+ ].join("\n") : workPhase === "build" ? [
35297
35356
  "",
35298
35357
  "# Work Phase: BUILD",
35299
- "Implement the approved plan. Prefer acting over describing. Update plan task statuses as you go."
35300
- ].join("\n") : "";
35358
+ "Implement on disk. Prefer acting over describing.",
35359
+ "- Prior plan/synthesis text is a SPEC to apply \u2014 not proof files already changed.",
35360
+ "- You MUST use write_file/edit_file for every file you change before claiming done.",
35361
+ "- After read_file: if the planned change is missing, WRITE it \u2014 do not stop at analysis.",
35362
+ "- Never claim already-implemented based only on reading a plan or skimming code.",
35363
+ planSummary ? "- An approved plan exists in the workspace \u2014 implement it and update task statuses as you go." : ""
35364
+ ].filter(Boolean).join("\n") : "";
35301
35365
  const shellContextBlock = [
35302
35366
  "# Platform & Shell",
35303
35367
  `platform: ${process.platform}`,
@@ -39471,7 +39535,21 @@ function parseHeadlessFlags(argv) {
39471
39535
  const parsedHist = JSON.parse(raw);
39472
39536
  if (Array.isArray(parsedHist)) {
39473
39537
  history2 = parsedHist.filter(
39474
- (m) => m && typeof m === "object" && typeof m.role === "string" && typeof m.content === "string"
39538
+ (m) => !!m && typeof m === "object" && typeof m.role === "string"
39539
+ ).map((m) => {
39540
+ const role = String(m.role);
39541
+ const raw2 = m.content;
39542
+ const content = typeof raw2 === "string" ? raw2 : raw2 == null ? "" : typeof raw2 === "object" ? JSON.stringify(raw2) : String(raw2);
39543
+ const msg = {
39544
+ role,
39545
+ content
39546
+ };
39547
+ if (typeof m.toolCallId === "string") {
39548
+ msg.toolCallId = m.toolCallId;
39549
+ }
39550
+ return msg;
39551
+ }).filter(
39552
+ (m) => m.role === "user" || m.role === "assistant" || m.role === "tool" || m.role === "system"
39475
39553
  );
39476
39554
  }
39477
39555
  } catch {
@@ -39535,6 +39613,7 @@ function emitEvent(event) {
39535
39613
  // src/cli/runHeadless.ts
39536
39614
  init_harness();
39537
39615
  init_conversationContext();
39616
+ init_council();
39538
39617
  init_toolRegistry();
39539
39618
  init_skills2();
39540
39619
  init_envNumber();
@@ -39708,7 +39787,19 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
39708
39787
  "The shell is NON-INTERACTIVE (stdin closed): pass non-interactive flags (--yes, --force, --template).",
39709
39788
  "",
39710
39789
  `# Work phase: ${opts.phase ?? "build"}`,
39711
- (opts.phase ?? "build") === "plan" ? "PLAN phase: explore and design only. Do not write project source files (write_file/edit_file/bash blocked). Plan artifacts under .zelari are allowed." : "BUILD phase: full tools; implement changes."
39790
+ (opts.phase ?? "build") === "plan" ? [
39791
+ "PLAN phase: explore and design only.",
39792
+ "Do not write project source files (write_file/edit_file/bash blocked).",
39793
+ "Plan artifacts under .zelari are allowed.",
39794
+ "When the plan is ready, tell the user to switch to BUILD to implement on disk."
39795
+ ].join(" ") : [
39796
+ "BUILD phase \u2014 IMPLEMENT ON DISK (mandatory when the user wants code/file changes).",
39797
+ "Prior chat may contain a plan or synthesis: that text is a SPEC to apply, NOT proof that files already changed.",
39798
+ "You MUST call write_file and/or edit_file for every file you change before saying you are done.",
39799
+ "After read_file: if the planned change is missing, WRITE it \u2014 do not stop at analysis.",
39800
+ 'Never claim "already implemented" / "tutto fatto" based only on reading a plan or skimming code.',
39801
+ "Only claim done after successful mutating tool calls in THIS turn (or after proving the exact planned diff already exists on disk via read_file of the real files)."
39802
+ ].join(" ")
39712
39803
  ].join("\n")
39713
39804
  };
39714
39805
  const { loadProjectInstructions: loadProjectInstructions2 } = await Promise.resolve().then(() => (init_projectInstructions(), projectInstructions_exports));
@@ -39753,95 +39844,196 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
39753
39844
  languageDirectiveContent
39754
39845
  ].join("\n");
39755
39846
  }
39756
- const historySeed = (opts.history ?? []).map(
39757
- (m) => m.role === "assistant" && m.content ? { ...m, content: cleanAgentContent(m.content, { stripQuestion: false }) } : m
39758
- );
39759
- const effectiveTask = maybeAnchorShortAnswer(opts.task) ?? opts.task;
39760
- const historySeedLen = historySeed.length;
39761
- const harness = new AgentHarness({
39762
- model,
39763
- provider,
39764
- sessionId,
39765
- messages: [
39766
- { role: "system", content: systemPrompt },
39767
- ...historySeed,
39768
- { role: "user", content: effectiveTask }
39769
- ],
39770
- tools,
39771
- toolRegistry,
39772
- providerStream,
39773
- maxToolLoopIterations: (() => {
39774
- const n = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 30, min: 1 });
39775
- return n;
39776
- })()
39777
- });
39778
- let finalReason = "completed";
39779
- let exitCode = 0;
39780
- const textBuffer = [];
39781
- const scrub = createStreamScrubber();
39782
- try {
39783
- for await (const event of harness.run()) {
39784
- if (event.type === "message_start") {
39785
- scrub.reset();
39786
- }
39787
- if (event.type === "message_delta" && typeof event.delta === "string") {
39788
- const cleanDelta = scrub.push(event.delta);
39789
- if (opts.output === "json") {
39790
- if (cleanDelta.length > 0) {
39791
- emitEvent({ ...event, delta: cleanDelta });
39792
- }
39793
- } else if (opts.output === "plain") {
39794
- if (cleanDelta.length > 0) process.stdout.write(cleanDelta);
39795
- } else {
39796
- if (cleanDelta.length > 0) textBuffer.push(cleanDelta);
39847
+ const historySeed = (opts.history ?? []).filter((m) => m.role === "user" || m.role === "assistant").map(
39848
+ (m) => m.role === "assistant" && m.content ? {
39849
+ role: "assistant",
39850
+ content: cleanAgentContent(m.content, { stripQuestion: false })
39851
+ } : { role: m.role, content: m.content ?? "" }
39852
+ ).filter((m) => (m.content ?? "").trim().length > 0);
39853
+ const effectiveTask = buildAgentUserWithHistory(opts.task, historySeed);
39854
+ const maxToolLoop = (() => {
39855
+ const n = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, {
39856
+ default: 30,
39857
+ min: 1
39858
+ });
39859
+ return n;
39860
+ })();
39861
+ async function runSinglePass(messages, passSessionId) {
39862
+ const harness = new AgentHarness({
39863
+ model,
39864
+ provider,
39865
+ sessionId: passSessionId,
39866
+ messages,
39867
+ tools,
39868
+ toolRegistry,
39869
+ providerStream,
39870
+ maxToolLoopIterations: maxToolLoop
39871
+ });
39872
+ let finalReason = "completed";
39873
+ let exitCode = 0;
39874
+ const textBuffer = [];
39875
+ let successfulWrites = 0;
39876
+ let emittedWrites = 0;
39877
+ const pendingToolNames = /* @__PURE__ */ new Map();
39878
+ const scrub = createStreamScrubber();
39879
+ try {
39880
+ for await (const event of harness.run()) {
39881
+ if (event.type === "message_start") {
39882
+ scrub.reset();
39797
39883
  }
39798
- } else {
39799
- if (opts.output === "json") {
39800
- emitEvent(event);
39884
+ if (event.type === "tool_execution_start") {
39885
+ const name = event.toolName ?? "";
39886
+ const id = event.toolCallId ?? "";
39887
+ if (id && name) pendingToolNames.set(id, name);
39888
+ if (name === "write_file" || name === "edit_file" || name === "apply_diff") {
39889
+ emittedWrites += 1;
39890
+ }
39891
+ }
39892
+ if (event.type === "tool_execution_end") {
39893
+ const id = event.toolCallId ?? "";
39894
+ const name = pendingToolNames.get(id) ?? "";
39895
+ pendingToolNames.delete(id);
39896
+ const isError = !!event.isError;
39897
+ const result = String(event.result ?? "");
39898
+ if ((name === "write_file" || name === "edit_file" || name === "apply_diff") && !isError) {
39899
+ const zeroEdit = name === "edit_file" && /occurrencesReplaced["']?\s*[:=]\s*0\b|0 occurrence|no changes/i.test(
39900
+ result
39901
+ );
39902
+ if (!zeroEdit) successfulWrites += 1;
39903
+ }
39801
39904
  }
39802
- if (event.type === "agent_end") {
39803
- const tail = scrub.flush();
39804
- if (tail.length > 0) {
39805
- if (opts.output === "plain") process.stdout.write(tail);
39806
- else textBuffer.push(tail);
39905
+ if (event.type === "message_delta" && typeof event.delta === "string") {
39906
+ const cleanDelta = scrub.push(event.delta);
39907
+ if (opts.output === "json") {
39908
+ if (cleanDelta.length > 0) {
39909
+ emitEvent({ ...event, delta: cleanDelta });
39910
+ }
39911
+ } else if (opts.output === "plain") {
39912
+ if (cleanDelta.length > 0) process.stdout.write(cleanDelta);
39913
+ } else {
39914
+ if (cleanDelta.length > 0) textBuffer.push(cleanDelta);
39807
39915
  }
39808
- finalReason = event.reason;
39809
- if (event.reason === "error") exitCode = 3;
39810
- } else if (event.type === "error") {
39811
- if (event.severity === "fatal") {
39812
- exitCode = 2;
39916
+ } else {
39917
+ if (opts.output === "json") {
39918
+ emitEvent(event);
39919
+ }
39920
+ if (event.type === "agent_end") {
39921
+ const tail = scrub.flush();
39922
+ if (tail.length > 0) {
39923
+ if (opts.output === "plain") process.stdout.write(tail);
39924
+ else textBuffer.push(tail);
39925
+ }
39926
+ finalReason = event.reason;
39927
+ if (event.reason === "error") exitCode = 3;
39928
+ } else if (event.type === "error") {
39929
+ if (event.severity === "fatal") {
39930
+ exitCode = 2;
39931
+ }
39813
39932
  }
39814
39933
  }
39815
39934
  }
39935
+ } catch (err) {
39936
+ process.stderr.write(
39937
+ `[zelari-code --headless] runtime error: ${err instanceof Error ? err.message : String(err)}
39938
+ `
39939
+ );
39940
+ return {
39941
+ finalReason: "error",
39942
+ exitCode: 2,
39943
+ textBuffer,
39944
+ successfulWrites,
39945
+ emittedWrites,
39946
+ messages: harness.getMessages()
39947
+ };
39816
39948
  }
39817
- } catch (err) {
39818
- process.stderr.write(
39819
- `[zelari-code --headless] runtime error: ${err instanceof Error ? err.message : String(err)}
39949
+ return {
39950
+ finalReason,
39951
+ exitCode,
39952
+ textBuffer,
39953
+ successfulWrites,
39954
+ emittedWrites,
39955
+ messages: harness.getMessages()
39956
+ };
39957
+ }
39958
+ const initialMessages = [
39959
+ { role: "system", content: systemPrompt },
39960
+ ...historySeed,
39961
+ { role: "user", content: effectiveTask }
39962
+ ];
39963
+ let pass = await runSinglePass(initialMessages, sessionId);
39964
+ const wantWrites = expectsDiskImplementation(
39965
+ opts.task,
39966
+ opts.phase,
39967
+ historySeed
39968
+ );
39969
+ if (wantWrites && pass.successfulWrites === 0 && pass.finalReason === "completed" && pass.exitCode === 0) {
39970
+ const retryPrompt = buildImplementationWriteRetryPrompt(opts.task);
39971
+ if (opts.output === "json") {
39972
+ emitEvent({
39973
+ type: "log",
39974
+ message: "[headless] BUILD: no successful write_file/edit_file \u2014 forcing implementation retry"
39975
+ });
39976
+ } else {
39977
+ process.stderr.write(
39978
+ `[zelari-code --headless] BUILD: no successful writes \u2014 forcing implementation retry
39820
39979
  `
39821
- );
39822
- return 2;
39980
+ );
39981
+ }
39982
+ const retryMessages = [
39983
+ ...pass.messages.filter((m) => m.role !== "system")
39984
+ ];
39985
+ const withSystem = [
39986
+ { role: "system", content: systemPrompt },
39987
+ ...retryMessages,
39988
+ { role: "user", content: retryPrompt }
39989
+ ];
39990
+ const retry = await runSinglePass(withSystem, `${sessionId}-write-retry`);
39991
+ pass = {
39992
+ ...retry,
39993
+ textBuffer: [...pass.textBuffer, ...retry.textBuffer],
39994
+ successfulWrites: pass.successfulWrites + retry.successfulWrites,
39995
+ emittedWrites: pass.emittedWrites + retry.emittedWrites
39996
+ };
39823
39997
  }
39824
- if (opts.output === "plain" && textBuffer.length > 0) {
39825
- process.stdout.write(textBuffer.join(""));
39998
+ if (opts.output === "plain" && pass.textBuffer.length > 0) {
39999
+ process.stdout.write(pass.textBuffer.join(""));
39826
40000
  }
39827
40001
  process.stdout.write("");
39828
- if (finalReason !== "error" && opts.output === "json") {
40002
+ if (pass.finalReason !== "error" && opts.output === "json") {
39829
40003
  try {
39830
- const all = harness.getMessages();
39831
- const seedLen = 1 + historySeedLen + 1;
39832
- if (all.length > seedLen) {
39833
- const tail = all.slice(seedLen).map(
39834
- (m) => m.role === "assistant" && m.content ? { ...m, content: cleanAgentContent(m.content, { stripQuestion: false }) } : m
39835
- );
39836
- if (tail.length > 0) {
39837
- emitEvent({ type: "history_snapshot", messages: tail });
40004
+ const all = pass.messages;
40005
+ let lastAsst = "";
40006
+ for (let i = all.length - 1; i >= 0; i--) {
40007
+ const m = all[i];
40008
+ if (m?.role === "assistant" && (m.content ?? "").trim()) {
40009
+ lastAsst = cleanAgentContent(m.content, { stripQuestion: false });
40010
+ break;
39838
40011
  }
39839
40012
  }
40013
+ if (!lastAsst.trim() && pass.textBuffer.length > 0) {
40014
+ lastAsst = pass.textBuffer.join("").trim();
40015
+ }
40016
+ if (wantWrites && pass.successfulWrites === 0) {
40017
+ lastAsst = (lastAsst ? `${lastAsst}
40018
+
40019
+ ` : "") + "[zelari] WARNING: BUILD turn ended with zero successful file writes. The planned changes may still need to be applied on disk.";
40020
+ emitEvent({
40021
+ type: "log",
40022
+ message: "[headless] BUILD warning: still zero successful writes after retry"
40023
+ });
40024
+ }
40025
+ const snapshot = [
40026
+ { role: "user", content: opts.task },
40027
+ ...lastAsst ? [{ role: "assistant", content: lastAsst }] : []
40028
+ ];
40029
+ if (snapshot.length > 0) {
40030
+ emitEvent({ type: "history_snapshot", messages: snapshot });
40031
+ }
39840
40032
  } catch {
39841
40033
  }
39842
40034
  }
39843
- if (finalReason === "error") return 3;
39844
- return exitCode;
40035
+ if (pass.finalReason === "error") return 3;
40036
+ return pass.exitCode;
39845
40037
  }
39846
40038
  async function buildCouncilToolRegistry(planMode, opts) {
39847
40039
  const { registry: toolRegistry } = createBuiltinToolRegistry({ planMode });