zelari-code 1.22.1 → 1.24.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.
@@ -21,6 +21,87 @@ var __copyProps = (to, from, except, desc) => {
21
21
  };
22
22
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
23
 
24
+ // src/cli/modelPricing.ts
25
+ function envOverride(model) {
26
+ const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
27
+ const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
28
+ return parseRateOverride(raw);
29
+ }
30
+ function parseRateOverride(raw) {
31
+ if (!raw) return null;
32
+ const [inStr, outStr] = raw.split("/");
33
+ const input = Number.parseFloat(inStr);
34
+ const output = outStr !== void 0 ? Number.parseFloat(outStr) : input;
35
+ if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
36
+ return { input, output };
37
+ }
38
+ function getModelRate(model) {
39
+ if (!model) return DEFAULT_RATE;
40
+ return envOverride(model) ?? PRICES_PER_MILLION[model] ?? DEFAULT_RATE;
41
+ }
42
+ function calculateCost(model, promptTokens, completionTokens, cachedPromptTokens = 0) {
43
+ if (!Number.isFinite(promptTokens) || promptTokens < 0) return 0;
44
+ if (!Number.isFinite(completionTokens) || completionTokens < 0) return 0;
45
+ const rate = getModelRate(model);
46
+ const cached2 = Number.isFinite(cachedPromptTokens) && cachedPromptTokens > 0 ? Math.min(cachedPromptTokens, promptTokens) : 0;
47
+ const uncachedPrompt = promptTokens - cached2;
48
+ const cachedRate = rate.cachedInput ?? rate.input * DEFAULT_CACHE_DISCOUNT;
49
+ const inputCost = uncachedPrompt / 1e6 * rate.input + cached2 / 1e6 * cachedRate;
50
+ const outputCost = completionTokens / 1e6 * rate.output;
51
+ return Number((inputCost + outputCost).toFixed(6));
52
+ }
53
+ function formatCost(usd) {
54
+ if (!Number.isFinite(usd) || usd < 0) return "$0.0000";
55
+ if (usd === 0) return "$0.0000";
56
+ if (usd < 1e-4) return "<$0.0001";
57
+ return `$${usd.toFixed(4)}`;
58
+ }
59
+ function formatTokens(tokens) {
60
+ if (!Number.isFinite(tokens) || tokens < 0) return "0";
61
+ if (tokens < 1e3) return `${tokens}`;
62
+ if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
63
+ if (tokens < 1e9) return `${(tokens / 1e6).toFixed(2)}M`;
64
+ return `${(tokens / 1e9).toFixed(2)}B`;
65
+ }
66
+ var PRICES_PER_MILLION, DEFAULT_RATE, DEFAULT_CACHE_DISCOUNT;
67
+ var init_modelPricing = __esm({
68
+ "src/cli/modelPricing.ts"() {
69
+ "use strict";
70
+ PRICES_PER_MILLION = {
71
+ // xAI Grok (list prices; grok-4.5 default reasoning_effort is "high")
72
+ "grok-4.5": { input: 2, output: 6, cachedInput: 0.5 },
73
+ "grok-4.3": { input: 1.25, output: 2.5, cachedInput: 0.2 },
74
+ "grok-4": { input: 3, output: 15 },
75
+ "grok-4-fast": { input: 0.2, output: 0.5 },
76
+ "grok-3": { input: 3, output: 15 },
77
+ "grok-3-mini": { input: 0.3, output: 0.5 },
78
+ "grok-2-vision": { input: 2, output: 10 },
79
+ // GLM / Z.AI
80
+ "glm-4.6": { input: 0.6, output: 2.2 },
81
+ "glm-4.5": { input: 0.5, output: 2 },
82
+ "glm-4.5-air": { input: 0.1, output: 0.6 },
83
+ "glm-z1": { input: 0.5, output: 2 },
84
+ // MiniMax
85
+ "MiniMax-M2.5": { input: 0.2, output: 1.1 },
86
+ "MiniMax-M2": { input: 0.2, output: 1.1 },
87
+ "MiniMax-M2-her": { input: 0.3, output: 1.2 },
88
+ // DeepSeek (global platform) — estimated list prices; override via
89
+ // ANATHEMA_PRICE_DEEPSEEK_V4_FLASH / ANATHEMA_PRICE_DEEPSEEK_V4_PRO.
90
+ // DeepSeek prompt-cache hits are ~10× cheaper than a cache miss.
91
+ "deepseek-v4-flash": { input: 0.14, output: 0.28, cachedInput: 0.014 },
92
+ "deepseek-v4-pro": { input: 0.55, output: 2.19, cachedInput: 0.055 },
93
+ // OpenAI (for openai-compatible fallback)
94
+ "gpt-4o": { input: 2.5, output: 10 },
95
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
96
+ "gpt-4-turbo": { input: 10, output: 30 },
97
+ "o1-preview": { input: 15, output: 60 },
98
+ "o1-mini": { input: 3, output: 12 }
99
+ };
100
+ DEFAULT_RATE = { input: 1, output: 3 };
101
+ DEFAULT_CACHE_DISCOUNT = 0.25;
102
+ }
103
+ });
104
+
24
105
  // src/cli/grokOAuth.ts
25
106
  var grokOAuth_exports = {};
26
107
  __export(grokOAuth_exports, {
@@ -1543,10 +1624,10 @@ function mergeDefs(...defs) {
1543
1624
  function cloneDef(schema) {
1544
1625
  return mergeDefs(schema._zod.def);
1545
1626
  }
1546
- function getElementAtPath(obj, path38) {
1547
- if (!path38)
1627
+ function getElementAtPath(obj, path40) {
1628
+ if (!path40)
1548
1629
  return obj;
1549
- return path38.reduce((acc, key) => acc?.[key], obj);
1630
+ return path40.reduce((acc, key) => acc?.[key], obj);
1550
1631
  }
1551
1632
  function promiseAllObject(promisesObj) {
1552
1633
  const keys = Object.keys(promisesObj);
@@ -1874,11 +1955,11 @@ function explicitlyAborted(x, startIndex = 0) {
1874
1955
  }
1875
1956
  return false;
1876
1957
  }
1877
- function prefixIssues(path38, issues) {
1958
+ function prefixIssues(path40, issues) {
1878
1959
  return issues.map((iss) => {
1879
1960
  var _a3;
1880
1961
  (_a3 = iss).path ?? (_a3.path = []);
1881
- iss.path.unshift(path38);
1962
+ iss.path.unshift(path40);
1882
1963
  return iss;
1883
1964
  });
1884
1965
  }
@@ -2096,16 +2177,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2096
2177
  }
2097
2178
  function formatError(error51, mapper = (issue2) => issue2.message) {
2098
2179
  const fieldErrors = { _errors: [] };
2099
- const processError = (error52, path38 = []) => {
2180
+ const processError = (error52, path40 = []) => {
2100
2181
  for (const issue2 of error52.issues) {
2101
2182
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2102
- issue2.errors.map((issues) => processError({ issues }, [...path38, ...issue2.path]));
2183
+ issue2.errors.map((issues) => processError({ issues }, [...path40, ...issue2.path]));
2103
2184
  } else if (issue2.code === "invalid_key") {
2104
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2185
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2105
2186
  } else if (issue2.code === "invalid_element") {
2106
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2187
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2107
2188
  } else {
2108
- const fullpath = [...path38, ...issue2.path];
2189
+ const fullpath = [...path40, ...issue2.path];
2109
2190
  if (fullpath.length === 0) {
2110
2191
  fieldErrors._errors.push(mapper(issue2));
2111
2192
  } else {
@@ -2132,17 +2213,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2132
2213
  }
2133
2214
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2134
2215
  const result = { errors: [] };
2135
- const processError = (error52, path38 = []) => {
2216
+ const processError = (error52, path40 = []) => {
2136
2217
  var _a3, _b;
2137
2218
  for (const issue2 of error52.issues) {
2138
2219
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2139
- issue2.errors.map((issues) => processError({ issues }, [...path38, ...issue2.path]));
2220
+ issue2.errors.map((issues) => processError({ issues }, [...path40, ...issue2.path]));
2140
2221
  } else if (issue2.code === "invalid_key") {
2141
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2222
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2142
2223
  } else if (issue2.code === "invalid_element") {
2143
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2224
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2144
2225
  } else {
2145
- const fullpath = [...path38, ...issue2.path];
2226
+ const fullpath = [...path40, ...issue2.path];
2146
2227
  if (fullpath.length === 0) {
2147
2228
  result.errors.push(mapper(issue2));
2148
2229
  continue;
@@ -2174,8 +2255,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2174
2255
  }
2175
2256
  function toDotPath(_path) {
2176
2257
  const segs = [];
2177
- const path38 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2178
- for (const seg of path38) {
2258
+ const path40 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2259
+ for (const seg of path40) {
2179
2260
  if (typeof seg === "number")
2180
2261
  segs.push(`[${seg}]`);
2181
2262
  else if (typeof seg === "symbol")
@@ -15678,13 +15759,13 @@ function resolveRef(ref, ctx) {
15678
15759
  if (!ref.startsWith("#")) {
15679
15760
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15680
15761
  }
15681
- const path38 = ref.slice(1).split("/").filter(Boolean);
15682
- if (path38.length === 0) {
15762
+ const path40 = ref.slice(1).split("/").filter(Boolean);
15763
+ if (path40.length === 0) {
15683
15764
  return ctx.rootSchema;
15684
15765
  }
15685
15766
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15686
- if (path38[0] === defsKey) {
15687
- const key = path38[1];
15767
+ if (path40[0] === defsKey) {
15768
+ const key = path40[1];
15688
15769
  if (!key || !ctx.defs[key]) {
15689
15770
  throw new Error(`Reference not found: ${ref}`);
15690
15771
  }
@@ -17848,11 +17929,11 @@ var init_tools = __esm({
17848
17929
  if (!ctx.addDocument)
17849
17930
  return "Knowledge vault tool not available.";
17850
17931
  const title = args["title"] || "New Document";
17851
- const path38 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
17932
+ const path40 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
17852
17933
  const content = args["content"] || "";
17853
17934
  const tags = args["tags"] || [];
17854
17935
  ctx.addDocument({
17855
- path: path38,
17936
+ path: path40,
17856
17937
  title,
17857
17938
  content,
17858
17939
  format: "markdown",
@@ -17861,7 +17942,7 @@ var init_tools = __esm({
17861
17942
  workspaceId: ctx.workspaceId
17862
17943
  });
17863
17944
  ctx.addActivity("vault", "created document", title);
17864
- return `Document "${title}" created at "${path38}".`;
17945
+ return `Document "${title}" created at "${path40}".`;
17865
17946
  }
17866
17947
  }
17867
17948
  ];
@@ -18357,6 +18438,7 @@ function getBasePromptModules(mode = "council") {
18357
18438
  BEHAVIOR_AGENT,
18358
18439
  SAFETY,
18359
18440
  CODING_PRACTICES_MODULE,
18441
+ TURN_COMPLETION_MODULE,
18360
18442
  OUTPUT_QUALITY_DIRECTIVE,
18361
18443
  OUTPUT_FORMATTING,
18362
18444
  // Same structured clarification format as council — one question when blocked.
@@ -18382,7 +18464,7 @@ function getBasePromptModules(mode = "council") {
18382
18464
  function getPromptModule(type) {
18383
18465
  return getBasePromptModules("council").find((m) => m.type === type);
18384
18466
  }
18385
- var CODING_CAPABLE_IDENTITY, COUNCIL_IDENTITY, BEHAVIOR_AGENT, BEHAVIOR_COUNCIL, SAFETY, CONTEXT_SHARING_COUNCIL, OUTPUT_FORMATTING, NATIVE_TOOL_PROTOCOL_MODULE, CODING_PRACTICES_MODULE, CLARIFICATION_PROTOCOL_MODULE, PROMPT_MODULES, SINGLE_AGENT_IDENTITY_MODULE;
18467
+ var CODING_CAPABLE_IDENTITY, COUNCIL_IDENTITY, BEHAVIOR_AGENT, BEHAVIOR_COUNCIL, SAFETY, CONTEXT_SHARING_COUNCIL, OUTPUT_FORMATTING, NATIVE_TOOL_PROTOCOL_MODULE, CODING_PRACTICES_MODULE, TURN_COMPLETION_MODULE, CLARIFICATION_PROTOCOL_MODULE, PROMPT_MODULES, SINGLE_AGENT_IDENTITY_MODULE;
18386
18468
  var init_promptModules = __esm({
18387
18469
  "packages/core/dist/agents/promptModules.js"() {
18388
18470
  "use strict";
@@ -18418,7 +18500,9 @@ Earlier members' outputs are shared context. Build on them; do not re-derive or
18418
18500
  - 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.
18419
18501
  - When the user confirms a plan ("procedi", "s\xEC", "implementa"), the prior plan is work TO DO on disk. Reading alone is incomplete.
18420
18502
  - Never claim work is "already implemented" without verifying the real files (and writing if gaps remain).
18421
- - Think step by step internally; surface conclusions and a brief rationale, not a full chain of thought.`
18503
+ - Think step by step internally; surface conclusions and a brief rationale, not a full chain of thought.
18504
+ - **No status theater**: never re-emit the same "I will create X / updating todos / next file Y" paragraph. Either call tools or stop.
18505
+ - **Turn must end cleanly** (see Turn Completion Contract): finish the requested slice, or stop with a short report and ask whether to continue \u2014 never hang in a loop of intentions.`
18422
18506
  };
18423
18507
  BEHAVIOR_COUNCIL = {
18424
18508
  type: "behavior-rules",
@@ -18480,6 +18564,7 @@ Earlier members' outputs are shared context. Build on them; do not re-derive or
18480
18564
  - Prefer tools over asking the user to paste file contents.
18481
18565
  - After durable changes, briefly name what you created or modified.
18482
18566
  - **Act, don't narrate**: if you will edit/fix files, call the tools in this turn. Do not restate the same diagnosis or "I will fix\u2026" plan on a loop without tool calls.
18567
+ - **Ban status loops**: phrases like "Aggiorno todo", "Procedo con", "Ora creo", "Next I will write" must be followed by a real tool call in the same turn \u2014 or stop and ask to continue.
18483
18568
  - Text-only tool blocks (\`---TOOLS---\` JSON) are a legacy fallback \u2014 use them only if the runtime has no native tool channel.`
18484
18569
  };
18485
18570
  CODING_PRACTICES_MODULE = {
@@ -18496,7 +18581,37 @@ Earlier members' outputs are shared context. Build on them; do not re-derive or
18496
18581
  - **Use project scripts**: prefer package.json / Makefile / existing tooling over ad-hoc commands.
18497
18582
  - **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.
18498
18583
  - **No spam**: never repeat the same paragraph or status line. One diagnosis, then tools (or one short final answer).
18584
+ - **Scope large builds**: for multi-file / game / MVP work, implement a thin vertical slice per turn (e.g. one module + wire-up). Do not try to ship an entire product in one endless monologue.
18499
18585
  - **Browser smoke honesty**: with \`browser_check\`, prefer \`waitForSelector\` / \`waitForText\` / \`evaluate\` on DOM (or explicit test hooks). Do not claim a logic fix is verified from \u201Cno console errors after N seconds\u201D alone (\`smokeStrength: weak\`). ES modules keep symbols off \`window\` \u2014 do not loop on exposing globals; assert visible UI or inject \`evaluate\` on \`document\`.`
18586
+ };
18587
+ TURN_COMPLETION_MODULE = {
18588
+ type: "custom",
18589
+ title: "Turn Completion",
18590
+ priority: 48,
18591
+ content: `# Turn Completion Contract (mandatory)
18592
+
18593
+ Every assistant turn MUST end in exactly one of these ways:
18594
+
18595
+ ## A) Done
18596
+ - You finished the user's request (or the agreed slice).
18597
+ - List paths changed + how to verify.
18598
+ - Stop. Do not start the next major feature unless asked.
18599
+
18600
+ ## B) Checkpoint \u2014 ask to continue
18601
+ - You made real progress (tools ran; files on disk) but more remains.
18602
+ - Give a short resoconto: what is done, what is next (3\u20136 bullets max).
18603
+ - **Ask the user** whether to continue (prefer \`ask_user\` with choices like Continue / Stop / Change priority).
18604
+ - Do **not** silently start the next big module in the same turn after a long status monologue.
18605
+
18606
+ ## C) Blocked \u2014 one question
18607
+ - Use the Clarification Protocol once, then wait.
18608
+
18609
+ ## Forbidden
18610
+ - Repeating "I will create X / updating todos / next I will write Y" without tool calls.
18611
+ - Narrating a multi-file roadmap and never writing, or writing forever without a stop.
18612
+ - Ending a turn mid-"procedo con\u2026" loop.
18613
+
18614
+ If the remaining work is large, **choose B** early: deliver one solid slice, report, ask.`
18500
18615
  };
18501
18616
  CLARIFICATION_PROTOCOL_MODULE = {
18502
18617
  type: "custom",
@@ -18544,7 +18659,7 @@ You are Zelari Code, an interactive AI coding agent in the user's terminal (or d
18544
18659
 
18545
18660
  You ARE connected to this machine and have real tools to read, modify, and explore the codebase. Never claim you lack filesystem or shell access \u2014 you have it. Use tools instead of asking the user to paste file contents.
18546
18661
 
18547
- Be proactive: list and read key files before changing code. When you finish, briefly summarize what you did and how to verify it.`
18662
+ Be proactive: list and read key files before changing code. When you finish a slice, briefly summarize what you did and how to verify it. If more work remains, stop with a short resoconto and ask whether to continue \u2014 do not monologue forever.`
18548
18663
  };
18549
18664
  }
18550
18665
  });
@@ -19241,17 +19356,27 @@ var init_events = __esm({
19241
19356
  });
19242
19357
 
19243
19358
  // packages/core/dist/core/textLoopDetect.js
19359
+ function isStatusTheaterUnit(unit) {
19360
+ const u = normalizeLoopUnit(unit).toLowerCase();
19361
+ if (u.length < 32)
19362
+ return false;
19363
+ return /\b(procedo|procediamo|continuo|continuiamo)\b/.test(u) || /\b(aggiorno|updating)\s+(il\s+)?todo\b/.test(u) || /\bora\s+(creo|creo|scrivo|implemento|faccio)\b/.test(u) || /\b(next\s+i\s+will|i\s+will\s+(now\s+)?(create|write|implement|update))\b/.test(u) || /\b(let\s+me\s+(now\s+)?(create|write|update|proceed))\b/.test(u) || /\b(fatto\.?\s*(ora|next|procedo)|bene\.?\s*(ora|procedo|aggiorno))\b/.test(u) || /\b(todo\s+(list\s+)?updated|moving\s+on\s+to)\b/.test(u);
19364
+ }
19244
19365
  function normalizeLoopUnit(s) {
19245
19366
  return s.replace(/<\/?small>/gi, "").replace(/<\/?(?:p|div|span|b|i|em|strong|br)\b[^>]*>/gi, "").replace(/\s+/g, " ").trim();
19246
19367
  }
19247
19368
  function detectChunkSequenceLoop(chunks, kind) {
19248
- if (chunks.length < MIN_REPEATS)
19369
+ if (chunks.length < MIN_REPEATS_STATUS)
19249
19370
  return { looping: false };
19250
- const maxK = Math.min(12, Math.floor(chunks.length / MIN_REPEATS));
19371
+ const maxK = Math.min(12, Math.floor(chunks.length / MIN_REPEATS_STATUS));
19251
19372
  for (let k = 1; k <= maxK; k++) {
19252
19373
  const unitParts = chunks.slice(chunks.length - k);
19253
19374
  const unitKey = unitParts.join("\n");
19254
- if (normalizeLoopUnit(unitKey).length < MIN_UNIT)
19375
+ const norm = normalizeLoopUnit(unitKey);
19376
+ if (norm.length < MIN_UNIT)
19377
+ continue;
19378
+ const need = isStatusTheaterUnit(unitKey) ? MIN_REPEATS_STATUS : MIN_REPEATS;
19379
+ if (chunks.length < need * k)
19255
19380
  continue;
19256
19381
  let count = 1;
19257
19382
  let pos = chunks.length - k;
@@ -19269,7 +19394,7 @@ function detectChunkSequenceLoop(chunks, kind) {
19269
19394
  count++;
19270
19395
  pos -= k;
19271
19396
  }
19272
- if (count >= MIN_REPEATS) {
19397
+ if (count >= need) {
19273
19398
  return {
19274
19399
  looping: true,
19275
19400
  unit: unitKey,
@@ -19282,19 +19407,20 @@ function detectChunkSequenceLoop(chunks, kind) {
19282
19407
  }
19283
19408
  function detectSuffixPeriod(text) {
19284
19409
  const compact = text.replace(/\s+/g, " ").trim();
19285
- if (compact.length < MIN_UNIT * MIN_REPEATS)
19410
+ if (compact.length < MIN_UNIT * MIN_REPEATS_STATUS)
19286
19411
  return { looping: false };
19287
19412
  const tail = compact.slice(-Math.min(compact.length, MAX_TAIL));
19288
- const maxP = Math.min(MAX_PERIOD, Math.floor(tail.length / MIN_REPEATS));
19413
+ const maxP = Math.min(MAX_PERIOD, Math.floor(tail.length / MIN_REPEATS_STATUS));
19289
19414
  for (let period = MIN_UNIT; period <= maxP; period++) {
19290
- const need = period * MIN_REPEATS;
19291
- if (tail.length < need)
19292
- continue;
19293
19415
  const unit = tail.slice(tail.length - period);
19294
19416
  if (normalizeLoopUnit(unit).length < MIN_UNIT * 0.55)
19295
19417
  continue;
19418
+ const need = isStatusTheaterUnit(unit) ? MIN_REPEATS_STATUS : MIN_REPEATS;
19419
+ const needChars = period * need;
19420
+ if (tail.length < needChars)
19421
+ continue;
19296
19422
  let ok = true;
19297
- for (let r = 1; r < MIN_REPEATS; r++) {
19423
+ for (let r = 1; r < need; r++) {
19298
19424
  const start = tail.length - period * (r + 1);
19299
19425
  if (tail.slice(start, start + period) !== unit) {
19300
19426
  ok = false;
@@ -19303,8 +19429,8 @@ function detectSuffixPeriod(text) {
19303
19429
  }
19304
19430
  if (!ok)
19305
19431
  continue;
19306
- let count = MIN_REPEATS;
19307
- let end = tail.length - period * MIN_REPEATS;
19432
+ let count = need;
19433
+ let end = tail.length - period * need;
19308
19434
  while (end >= period && tail.slice(end - period, end) === unit) {
19309
19435
  count++;
19310
19436
  end -= period;
@@ -19319,7 +19445,7 @@ function detectSuffixPeriod(text) {
19319
19445
  return { looping: false };
19320
19446
  }
19321
19447
  function detectAssistantTextLoop(text) {
19322
- if (!text || text.length < MIN_UNIT * MIN_REPEATS) {
19448
+ if (!text || text.length < MIN_UNIT * MIN_REPEATS_STATUS) {
19323
19449
  return { looping: false };
19324
19450
  }
19325
19451
  const nl = text.replace(/\r\n/g, "\n");
@@ -19339,7 +19465,8 @@ function collapseLoopedAssistantText(text) {
19339
19465
  return text;
19340
19466
  const note = `
19341
19467
 
19342
- [system: stopped repeating the same text \xD7${hit.count}; call tools or finish.]`;
19468
+ [system: stopped repeating the same text \xD7${hit.count}.]
19469
+ ` + TEXT_LOOP_RECOVERY_SYSTEM;
19343
19470
  if (hit.kind === "paragraph" || hit.kind === "line") {
19344
19471
  const joinSep = hit.kind === "paragraph" ? "\n\n" : "\n";
19345
19472
  const unitParts = hit.unit.split("\n").map((p3) => normalizeLoopUnit(p3));
@@ -19347,7 +19474,8 @@ function collapseLoopedAssistantText(text) {
19347
19474
  if (k === 0)
19348
19475
  return text + note;
19349
19476
  const rawParts = text.replace(/\r\n/g, "\n").split(hit.kind === "paragraph" ? /\n\s*\n+/ : /\n/).filter((p3) => normalizeLoopUnit(p3).length > 0);
19350
- if (rawParts.length < k * MIN_REPEATS) {
19477
+ const minNeed = isStatusTheaterUnit(hit.unit) ? MIN_REPEATS_STATUS : MIN_REPEATS;
19478
+ if (rawParts.length < k * minNeed) {
19351
19479
  return collapseByCharBudget(text, hit.unit, hit.count) + note;
19352
19480
  }
19353
19481
  let trailBlocks = 0;
@@ -19365,10 +19493,10 @@ function collapseLoopedAssistantText(text) {
19365
19493
  trailBlocks++;
19366
19494
  pos -= k;
19367
19495
  }
19368
- if (trailBlocks < MIN_REPEATS) {
19496
+ if (trailBlocks < minNeed) {
19369
19497
  return collapseByCharBudget(text, hit.unit, hit.count) + note;
19370
19498
  }
19371
- const keepBlocks = 2;
19499
+ const keepBlocks = Math.min(2, trailBlocks);
19372
19500
  const keepParts = rawParts.slice(0, rawParts.length - k * (trailBlocks - keepBlocks));
19373
19501
  return keepParts.join(joinSep).trimEnd() + note;
19374
19502
  }
@@ -19376,18 +19504,38 @@ function collapseLoopedAssistantText(text) {
19376
19504
  }
19377
19505
  function collapseByCharBudget(text, unit, count) {
19378
19506
  const unitLen = Math.max(unit.length, MIN_UNIT);
19379
- if (count < MIN_REPEATS)
19507
+ if (count < 2)
19380
19508
  return text;
19381
- const drop = unitLen * (count - 2);
19509
+ const keep = Math.min(2, count);
19510
+ const drop = unitLen * (count - keep);
19382
19511
  const cut = Math.max(0, text.length - drop);
19383
19512
  return text.slice(0, cut).trimEnd();
19384
19513
  }
19385
- var MIN_UNIT, MIN_REPEATS, MAX_TAIL, MAX_PERIOD;
19514
+ var TEXT_LOOP_RECOVERY_SYSTEM, TEXT_LOOP_RECOVERY_USER_PROMPT, MIN_UNIT, MIN_REPEATS, MIN_REPEATS_STATUS, MAX_TAIL, MAX_PERIOD;
19386
19515
  var init_textLoopDetect = __esm({
19387
19516
  "packages/core/dist/core/textLoopDetect.js"() {
19388
19517
  "use strict";
19518
+ TEXT_LOOP_RECOVERY_SYSTEM = [
19519
+ "[text-loop recovery] Your previous reply was stopped: you repeated status text",
19520
+ "instead of finishing (no clean conclusion).",
19521
+ "",
19522
+ "On the next turn you MUST:",
19523
+ "1) list_files / read_file what already exists (do not re-plan the whole project).",
19524
+ "2) Apply at most ONE small disk change with write_file/edit_file if still needed.",
19525
+ "3) Then STOP with either:",
19526
+ " A) Done \u2014 paths changed + how to verify, or",
19527
+ " B) Short resoconto (done / next) and ask_user whether to continue.",
19528
+ '4) Forbidden: more "procedo con / aggiorno todo / ora creo" monologue without tools.'
19529
+ ].join("\n");
19530
+ TEXT_LOOP_RECOVERY_USER_PROMPT = [
19531
+ "Continue from the text-loop stop.",
19532
+ "Inspect disk, apply at most one missing piece with tools if needed,",
19533
+ "then either mark DONE with a short verify list OR give a brief resoconto and ask if I want you to continue.",
19534
+ "No status theater, no full rewrite."
19535
+ ].join(" ");
19389
19536
  MIN_UNIT = 48;
19390
19537
  MIN_REPEATS = 3;
19538
+ MIN_REPEATS_STATUS = 2;
19391
19539
  MAX_TAIL = 6e3;
19392
19540
  MAX_PERIOD = 600;
19393
19541
  }
@@ -20121,7 +20269,7 @@ ${shared2.content}`,
20121
20269
  if (loopHit.looping) {
20122
20270
  const loopErr = createBrainEvent("error", this.sessionId, {
20123
20271
  severity: "recoverable",
20124
- message: `Assistant text loop detected (same block \xD7${loopHit.count}). Stopped generation \u2014 model was repeating instead of calling tools or finishing. Retry or ask it to apply changes with tools.`,
20272
+ message: `Assistant text loop detected (same block \xD7${loopHit.count}). Stopped generation \u2014 model was repeating instead of calling tools or finishing. Recovery injected: next turn should use tools only (inspect disk \u2192 one write). Or send: "${TEXT_LOOP_RECOVERY_USER_PROMPT.slice(0, 80)}\u2026"`,
20125
20273
  code: "assistant_text_loop"
20126
20274
  });
20127
20275
  this.emit(loopErr);
@@ -20135,6 +20283,13 @@ ${shared2.content}`,
20135
20283
  ...turnReasoning.length > 0 ? { reasoningContent: turnReasoning } : {}
20136
20284
  });
20137
20285
  }
20286
+ const last = this.config.messages[this.config.messages.length - 1];
20287
+ if (!(last?.role === "system" && typeof last.content === "string" && last.content.includes("[text-loop recovery]"))) {
20288
+ this.config.messages.push({
20289
+ role: "system",
20290
+ content: TEXT_LOOP_RECOVERY_SYSTEM
20291
+ });
20292
+ }
20138
20293
  break;
20139
20294
  }
20140
20295
  }
@@ -20560,9 +20715,12 @@ __export(harness_exports, {
20560
20715
  AgentHarness: () => AgentHarness,
20561
20716
  DOOM_LOOP_THRESHOLD: () => DOOM_LOOP_THRESHOLD,
20562
20717
  SessionJsonlWriter: () => SessionJsonlWriter,
20718
+ TEXT_LOOP_RECOVERY_SYSTEM: () => TEXT_LOOP_RECOVERY_SYSTEM,
20719
+ TEXT_LOOP_RECOVERY_USER_PROMPT: () => TEXT_LOOP_RECOVERY_USER_PROMPT,
20563
20720
  collapseLoopedAssistantText: () => collapseLoopedAssistantText,
20564
20721
  detectAssistantTextLoop: () => detectAssistantTextLoop,
20565
20722
  hashToolCall: () => hashToolCall,
20723
+ isStatusTheaterUnit: () => isStatusTheaterUnit,
20566
20724
  normalizeLoopUnit: () => normalizeLoopUnit,
20567
20725
  normalizeTextToolArgs: () => normalizeTextToolArgs,
20568
20726
  parseMinimaxStyleToolCalls: () => parseMinimaxStyleToolCalls,
@@ -20982,9 +21140,9 @@ function spillToolOutput(fullText, meta3) {
20982
21140
  const rnd = randomBytes(3).toString("hex");
20983
21141
  const safeTool = (meta3?.toolName ?? "tool").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
20984
21142
  const file2 = `${stamp}-${safeTool}-${hash3}-${rnd}.txt`;
20985
- const path38 = join(dir, file2);
20986
- writeFileSync5(path38, fullText, "utf8");
20987
- return path38;
21143
+ const path40 = join(dir, file2);
21144
+ writeFileSync5(path40, fullText, "utf8");
21145
+ return path40;
20988
21146
  } catch {
20989
21147
  return null;
20990
21148
  }
@@ -21030,10 +21188,10 @@ function truncateToolResult(text, capOrOpts = TOOL_RESULT_LINE_CAP) {
21030
21188
  ${tail}`;
21031
21189
  }
21032
21190
  if (doSpill) {
21033
- const path38 = spillToolOutput(text, { toolName: opts.toolName });
21034
- if (path38) {
21191
+ const path40 = spillToolOutput(text, { toolName: opts.toolName });
21192
+ if (path40) {
21035
21193
  const spillNote = `
21036
- \u2026 [full output spilled to: ${path38} \u2014 re-read with read_file if you need the complete text] \u2026`;
21194
+ \u2026 [full output spilled to: ${path40} \u2014 re-read with read_file if you need the complete text] \u2026`;
21037
21195
  if (preview.includes("] \u2026\n")) {
21038
21196
  preview = preview.replace("] \u2026\n", `] \u2026${spillNote}
21039
21197
  `);
@@ -23465,21 +23623,21 @@ function normalizeAuth(auth) {
23465
23623
  return "agent";
23466
23624
  }
23467
23625
  function readSecrets() {
23468
- const path38 = getSshSecretsPath();
23469
- if (!existsSync11(path38)) return {};
23626
+ const path40 = getSshSecretsPath();
23627
+ if (!existsSync11(path40)) return {};
23470
23628
  try {
23471
- return JSON.parse(readFileSync8(path38, "utf8"));
23629
+ return JSON.parse(readFileSync8(path40, "utf8"));
23472
23630
  } catch {
23473
23631
  return {};
23474
23632
  }
23475
23633
  }
23476
23634
  function writeSecrets(data) {
23477
- const path38 = getSshSecretsPath();
23478
- mkdirSync7(dirname2(path38), { recursive: true });
23479
- writeFileSync6(path38, `${JSON.stringify(data, null, 2)}
23635
+ const path40 = getSshSecretsPath();
23636
+ mkdirSync7(dirname2(path40), { recursive: true });
23637
+ writeFileSync6(path40, `${JSON.stringify(data, null, 2)}
23480
23638
  `, "utf8");
23481
23639
  try {
23482
- chmodSync(path38, 384);
23640
+ chmodSync(path40, 384);
23483
23641
  } catch {
23484
23642
  }
23485
23643
  }
@@ -23508,10 +23666,10 @@ function deleteSshPassword(id) {
23508
23666
  writeSecrets({ passwords });
23509
23667
  }
23510
23668
  function readStore2() {
23511
- const path38 = getSshTargetsPath();
23512
- if (!existsSync11(path38)) return [];
23669
+ const path40 = getSshTargetsPath();
23670
+ if (!existsSync11(path40)) return [];
23513
23671
  try {
23514
- const parsed = JSON.parse(readFileSync8(path38, "utf8"));
23672
+ const parsed = JSON.parse(readFileSync8(path40, "utf8"));
23515
23673
  const list = Array.isArray(parsed.targets) ? parsed.targets : [];
23516
23674
  return list.filter(
23517
23675
  (t) => t && typeof t.id === "string" && typeof t.host === "string" && typeof t.user === "string"
@@ -23526,11 +23684,11 @@ function readStore2() {
23526
23684
  }
23527
23685
  }
23528
23686
  function writeStore2(targets) {
23529
- const path38 = getSshTargetsPath();
23530
- mkdirSync7(dirname2(path38), { recursive: true });
23687
+ const path40 = getSshTargetsPath();
23688
+ mkdirSync7(dirname2(path40), { recursive: true });
23531
23689
  const clean = targets.map(({ hasPassword: _hp, ...t }) => t);
23532
23690
  writeFileSync6(
23533
- path38,
23691
+ path40,
23534
23692
  `${JSON.stringify({ targets: clean }, null, 2)}
23535
23693
  `,
23536
23694
  "utf8"
@@ -23776,11 +23934,11 @@ function formatSshTargetsForPrompt() {
23776
23934
  ];
23777
23935
  for (const t of targets) {
23778
23936
  const tags = t.tags?.length ? ` tags=[${t.tags.join(",")}]` : "";
23779
- const path38 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
23937
+ const path40 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
23780
23938
  const allow = t.allowedCommands?.length ? ` allowed=${t.allowedCommands.join("|")}` : " allowed=status-only";
23781
23939
  const auth = t.auth === "password" ? " auth=password" : t.auth === "keyPath" ? " auth=key" : " auth=agent";
23782
23940
  lines.push(
23783
- `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path38}${tags}${allow}`
23941
+ `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path40}${tags}${allow}`
23784
23942
  );
23785
23943
  }
23786
23944
  return lines.join("\n");
@@ -25780,11 +25938,11 @@ var init_synthesisAudit = __esm({
25780
25938
  import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
25781
25939
  import { join as join6 } from "node:path";
25782
25940
  function loadNfrSpec(zelariRoot) {
25783
- const path38 = join6(zelariRoot, "nfr-spec.json");
25784
- if (!existsSync13(path38))
25941
+ const path40 = join6(zelariRoot, "nfr-spec.json");
25942
+ if (!existsSync13(path40))
25785
25943
  return null;
25786
25944
  try {
25787
- const raw = JSON.parse(readFileSync10(path38, "utf8"));
25945
+ const raw = JSON.parse(readFileSync10(path40, "utf8"));
25788
25946
  if (raw.version !== 1 || !Array.isArray(raw.targets))
25789
25947
  return null;
25790
25948
  return raw;
@@ -28090,9 +28248,9 @@ var init_types3 = __esm({
28090
28248
  import { readFileSync as readFileSync15 } from "node:fs";
28091
28249
  import { join as join12 } from "node:path";
28092
28250
  function readLessonsDeduped(zelariRoot) {
28093
- const path38 = join12(zelariRoot, LESSONS_FILE);
28251
+ const path40 = join12(zelariRoot, LESSONS_FILE);
28094
28252
  try {
28095
- const raw = readFileSync15(path38, "utf8");
28253
+ const raw = readFileSync15(path40, "utf8");
28096
28254
  const byId = /* @__PURE__ */ new Map();
28097
28255
  for (const line of raw.split(/\r?\n/)) {
28098
28256
  if (!line.trim())
@@ -28193,8 +28351,8 @@ function keywordsFrom(check2, signature) {
28193
28351
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
28194
28352
  }
28195
28353
  function writeLesson(zelariRoot, lesson) {
28196
- const path38 = join13(zelariRoot, LESSONS_FILE);
28197
- appendFileSync2(path38, `${JSON.stringify(lesson)}
28354
+ const path40 = join13(zelariRoot, LESSONS_FILE);
28355
+ appendFileSync2(path40, `${JSON.stringify(lesson)}
28198
28356
  `, "utf8");
28199
28357
  }
28200
28358
  function findSimilar(lessons, signature) {
@@ -28630,6 +28788,7 @@ __export(council_exports, {
28630
28788
  STRUCTURED_REASONING_DIRECTIVE: () => STRUCTURED_REASONING_DIRECTIVE,
28631
28789
  TIER_RANK: () => TIER_RANK,
28632
28790
  TOOL_USE_PROTOCOL_DIRECTIVE: () => TOOL_USE_PROTOCOL_DIRECTIVE,
28791
+ TURN_COMPLETION_MODULE: () => TURN_COMPLETION_MODULE,
28633
28792
  UnknownMemberError: () => UnknownMemberError,
28634
28793
  applyCompletionRetry: () => applyCompletionRetry,
28635
28794
  applyDeterministicAutofix: () => applyDeterministicAutofix,
@@ -30350,28 +30509,28 @@ var init_storage = __esm({
30350
30509
  VALID_SCALARS = /^(true|false|null|~)$/i;
30351
30510
  Storage = class {
30352
30511
  /** Read a Markdown file with frontmatter. Throws if not found. */
30353
- read(path38) {
30354
- if (!existsSync23(path38)) {
30355
- throw new Error(`File not found: ${path38}`);
30512
+ read(path40) {
30513
+ if (!existsSync23(path40)) {
30514
+ throw new Error(`File not found: ${path40}`);
30356
30515
  }
30357
- const md = readFileSync20(path38, "utf8");
30516
+ const md = readFileSync20(path40, "utf8");
30358
30517
  return parseFrontmatter(md);
30359
30518
  }
30360
30519
  /** Read a Markdown file; returns null if not found. */
30361
- readIfExists(path38) {
30362
- if (!existsSync23(path38)) return null;
30363
- return this.read(path38);
30520
+ readIfExists(path40) {
30521
+ if (!existsSync23(path40)) return null;
30522
+ return this.read(path40);
30364
30523
  }
30365
30524
  /**
30366
30525
  * Write a Markdown file atomically (tmp + rename). Creates parent dirs.
30367
30526
  * The meta object is serialized as YAML frontmatter; body as Markdown.
30368
30527
  */
30369
- write(path38, meta3, body) {
30370
- mkdirSync9(dirname4(path38), { recursive: true });
30371
- const tmp = path38 + ".tmp-" + process.pid;
30528
+ write(path40, meta3, body) {
30529
+ mkdirSync9(dirname4(path40), { recursive: true });
30530
+ const tmp = path40 + ".tmp-" + process.pid;
30372
30531
  const md = serializeFrontmatter(meta3, body);
30373
30532
  writeFileSync13(tmp, md, "utf8");
30374
- renameSync2(tmp, path38);
30533
+ renameSync2(tmp, path40);
30375
30534
  }
30376
30535
  /** List all .md files in a directory (non-recursive). */
30377
30536
  listMarkdown(dir) {
@@ -30449,8 +30608,8 @@ function readPlan(ctx) {
30449
30608
  } catch {
30450
30609
  }
30451
30610
  }
30452
- const path38 = workspaceFile(ctx.rootDir, "plan");
30453
- const doc = ctx.storage.readIfExists(path38);
30611
+ const path40 = workspaceFile(ctx.rootDir, "plan");
30612
+ const doc = ctx.storage.readIfExists(path40);
30454
30613
  if (!doc) return { phases: [], tasks: [], milestones: [] };
30455
30614
  const meta3 = doc.meta;
30456
30615
  return {
@@ -30615,7 +30774,7 @@ function addMilestoneRecord(ctx, summary, input) {
30615
30774
  dueDate: input.dueDate,
30616
30775
  targetVersion: version2
30617
30776
  });
30618
- const path38 = join23(ctx.rootDir, "milestones", `${id}.md`);
30777
+ const path40 = join23(ctx.rootDir, "milestones", `${id}.md`);
30619
30778
  const meta3 = {
30620
30779
  kind: "milestone",
30621
30780
  id,
@@ -30632,7 +30791,7 @@ function addMilestoneRecord(ctx, summary, input) {
30632
30791
  `Target version: ${version2}`,
30633
30792
  ""
30634
30793
  ].join("\n");
30635
- ctx.storage.write(path38, meta3, body);
30794
+ ctx.storage.write(path40, meta3, body);
30636
30795
  return { id, created: true };
30637
30796
  }
30638
30797
  function readPlanSummary(ctx) {
@@ -30836,7 +30995,7 @@ function addIdeaStub(ctx) {
30836
30995
  const tags = args["tags"] ?? [];
30837
30996
  const category = args["category"] ?? "General";
30838
30997
  const id = `${nextAdrId(ctx)}-${slugify3(title)}`;
30839
- const path38 = workspaceArtifact(ctx.rootDir, "decisions", id);
30998
+ const path40 = workspaceArtifact(ctx.rootDir, "decisions", id);
30840
30999
  const meta3 = {
30841
31000
  kind: "adr",
30842
31001
  status: "proposed",
@@ -30862,7 +31021,7 @@ function addIdeaStub(ctx) {
30862
31021
  ...consequences.map((c) => `- ${c}`),
30863
31022
  ""
30864
31023
  ].join("\n");
30865
- ctx.storage.write(path38, meta3, body);
31024
+ ctx.storage.write(path40, meta3, body);
30866
31025
  return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
30867
31026
  });
30868
31027
  }
@@ -30944,14 +31103,14 @@ function createDocumentStub(ctx) {
30944
31103
  ctx.storage.write(risksPath, riskMeta, content);
30945
31104
  return `Document "${title}" created at risks.md (workspace root).`;
30946
31105
  }
30947
- const path38 = workspaceArtifact(ctx.rootDir, "docs", slug);
31106
+ const path40 = workspaceArtifact(ctx.rootDir, "docs", slug);
30948
31107
  const meta3 = {
30949
31108
  kind: "doc",
30950
31109
  id: slug,
30951
31110
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
30952
31111
  tags
30953
31112
  };
30954
- ctx.storage.write(path38, meta3, content);
31113
+ ctx.storage.write(path40, meta3, content);
30955
31114
  return `Document "${title}" created at docs/${slug}.md.`;
30956
31115
  });
30957
31116
  }
@@ -31495,10 +31654,10 @@ function getUserMcpPath() {
31495
31654
  function getProjectMcpPath(projectRoot) {
31496
31655
  return join24(projectRoot, ".zelari", "mcp.json");
31497
31656
  }
31498
- function readFile2(path38) {
31499
- if (!existsSync26(path38)) return {};
31657
+ function readFile2(path40) {
31658
+ if (!existsSync26(path40)) return {};
31500
31659
  try {
31501
- const parsed = JSON.parse(readFileSync22(path38, "utf8"));
31660
+ const parsed = JSON.parse(readFileSync22(path40, "utf8"));
31502
31661
  const out = {};
31503
31662
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
31504
31663
  if (!cfg || typeof cfg.command !== "string" || !cfg.command.trim()) continue;
@@ -31514,10 +31673,10 @@ function readFile2(path38) {
31514
31673
  return {};
31515
31674
  }
31516
31675
  }
31517
- function writeFile(path38, servers) {
31518
- mkdirSync11(dirname6(path38), { recursive: true });
31676
+ function writeFile(path40, servers) {
31677
+ mkdirSync11(dirname6(path40), { recursive: true });
31519
31678
  const body = { mcpServers: servers };
31520
- writeFileSync15(path38, `${JSON.stringify(body, null, 2)}
31679
+ writeFileSync15(path40, `${JSON.stringify(body, null, 2)}
31521
31680
  `, "utf8");
31522
31681
  }
31523
31682
  function listMcpServers(projectRoot) {
@@ -31550,9 +31709,9 @@ function upsertMcpServer(opts) {
31550
31709
  if (!opts.config.command?.trim()) {
31551
31710
  return { ok: false, error: "command is required" };
31552
31711
  }
31553
- let path38;
31712
+ let path40;
31554
31713
  if (opts.scope === "user") {
31555
- path38 = getUserMcpPath();
31714
+ path40 = getUserMcpPath();
31556
31715
  } else {
31557
31716
  const root = opts.projectRoot?.trim();
31558
31717
  if (!root) {
@@ -31561,30 +31720,30 @@ function upsertMcpServer(opts) {
31561
31720
  error: "projectRoot required for project scope (Open Folder first)"
31562
31721
  };
31563
31722
  }
31564
- path38 = getProjectMcpPath(root);
31723
+ path40 = getProjectMcpPath(root);
31565
31724
  }
31566
- const current = readFile2(path38);
31725
+ const current = readFile2(path40);
31567
31726
  current[name] = {
31568
31727
  command: opts.config.command.trim(),
31569
31728
  args: opts.config.args,
31570
31729
  env: opts.config.env,
31571
31730
  enabled: opts.config.enabled !== false
31572
31731
  };
31573
- writeFile(path38, current);
31574
- return { ok: true, path: path38 };
31732
+ writeFile(path40, current);
31733
+ return { ok: true, path: path40 };
31575
31734
  }
31576
31735
  function removeMcpServer(opts) {
31577
- const path38 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
31578
- if (!path38) {
31736
+ const path40 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
31737
+ if (!path40) {
31579
31738
  return { ok: false, error: "projectRoot required for project scope" };
31580
31739
  }
31581
- const current = readFile2(path38);
31740
+ const current = readFile2(path40);
31582
31741
  if (!(opts.name in current)) {
31583
- return { ok: false, error: `Server "${opts.name}" not found in ${path38}` };
31742
+ return { ok: false, error: `Server "${opts.name}" not found in ${path40}` };
31584
31743
  }
31585
31744
  delete current[opts.name];
31586
- writeFile(path38, current);
31587
- return { ok: true, path: path38 };
31745
+ writeFile(path40, current);
31746
+ return { ok: true, path: path40 };
31588
31747
  }
31589
31748
  var init_mcpConfigIo = __esm({
31590
31749
  "src/cli/mcp/mcpConfigIo.ts"() {
@@ -31921,10 +32080,10 @@ import { createHash as createHash5 } from "node:crypto";
31921
32080
  import { join as join26 } from "node:path";
31922
32081
  import { readFile as readFile3 } from "node:fs/promises";
31923
32082
  async function readPackageJson2(projectRoot) {
31924
- const path38 = join26(projectRoot, "package.json");
31925
- if (!existsSync28(path38)) return null;
32083
+ const path40 = join26(projectRoot, "package.json");
32084
+ if (!existsSync28(path40)) return null;
31926
32085
  try {
31927
- return JSON.parse(await readFile3(path38, "utf8"));
32086
+ return JSON.parse(await readFile3(path40, "utf8"));
31928
32087
  } catch {
31929
32088
  return null;
31930
32089
  }
@@ -32006,9 +32165,9 @@ async function genBuild(ctx) {
32006
32165
  ].join("\n");
32007
32166
  }
32008
32167
  async function genOpenQuestions(ctx) {
32009
- const path38 = join26(ctx.rootDir, "risks.md");
32010
- if (!existsSync28(path38)) return "_No open questions._";
32011
- const content = readFileSync24(path38, "utf8");
32168
+ const path40 = join26(ctx.rootDir, "risks.md");
32169
+ if (!existsSync28(path40)) return "_No open questions._";
32170
+ const content = readFileSync24(path40, "utf8");
32012
32171
  const lines = content.split("\n");
32013
32172
  const questions = [];
32014
32173
  let currentTitle = "";
@@ -32575,8 +32734,8 @@ async function runPostCouncilHook(ctx, options) {
32575
32734
  sources: scope.sources
32576
32735
  } : void 0
32577
32736
  });
32578
- const path38 = writeCouncilCompletion(ctx.rootDir, completion);
32579
- completionHook = { ran: true, path: path38, completion };
32737
+ const path40 = writeCouncilCompletion(ctx.rootDir, completion);
32738
+ completionHook = { ran: true, path: path40, completion };
32580
32739
  } catch (err) {
32581
32740
  completionHook = {
32582
32741
  ran: true,
@@ -33133,18 +33292,49 @@ var init_fileBackend = __esm({
33133
33292
  }
33134
33293
  });
33135
33294
 
33295
+ // src/cli/traceStore.ts
33296
+ import { promises as fs17 } from "node:fs";
33297
+ import * as path29 from "node:path";
33298
+ function traceDir(projectRoot) {
33299
+ return path29.join(projectRoot, ".zelari", "trace");
33300
+ }
33301
+ function tracePath(projectRoot, missionId) {
33302
+ return path29.join(traceDir(projectRoot), `${missionId}.json`);
33303
+ }
33304
+ async function saveTrace(projectRoot, missionId, entries) {
33305
+ const dir = traceDir(projectRoot);
33306
+ await fs17.mkdir(dir, { recursive: true });
33307
+ const payload = {
33308
+ missionId,
33309
+ ts: Date.now(),
33310
+ entries
33311
+ };
33312
+ await fs17.writeFile(
33313
+ tracePath(projectRoot, missionId),
33314
+ JSON.stringify(payload, null, 2) + "\n",
33315
+ "utf8"
33316
+ );
33317
+ }
33318
+ var init_traceStore = __esm({
33319
+ "src/cli/traceStore.ts"() {
33320
+ "use strict";
33321
+ }
33322
+ });
33323
+
33136
33324
  // src/cli/zelariMission.ts
33137
33325
  var zelariMission_exports = {};
33138
33326
  __export(zelariMission_exports, {
33139
33327
  formatBriefForChat: () => formatBriefForChat,
33140
33328
  isMissionAutoStart: () => isMissionAutoStart,
33329
+ resolveMaxCost: () => resolveMaxCost,
33141
33330
  resolveMaxIterations: () => resolveMaxIterations,
33142
33331
  resolveMaxStall: () => resolveMaxStall,
33332
+ resolveMaxTokens: () => resolveMaxTokens,
33143
33333
  runZelariMission: () => runZelariMission
33144
33334
  });
33145
33335
  import { randomUUID as randomUUID5 } from "node:crypto";
33146
- import { promises as fs17 } from "node:fs";
33147
- import * as path29 from "node:path";
33336
+ import { promises as fs18 } from "node:fs";
33337
+ import * as path30 from "node:path";
33148
33338
  function resolveMaxIterations(env = process.env) {
33149
33339
  const raw = env.ZELARI_MISSION_MAX_ITER;
33150
33340
  const n = raw ? Number.parseInt(raw, 10) : DEFAULT_MAX_ITER;
@@ -33156,17 +33346,35 @@ function resolveMaxStall(env = process.env) {
33156
33346
  const n = Number.parseInt(raw, 10);
33157
33347
  return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MAX_STALL;
33158
33348
  }
33349
+ function resolveMaxCost(env = process.env) {
33350
+ const raw = env.ZELARI_MISSION_MAX_COST;
33351
+ if (!raw) return void 0;
33352
+ const n = Number.parseFloat(raw);
33353
+ return Number.isFinite(n) && n > 0 ? n : void 0;
33354
+ }
33355
+ function resolveMaxTokens(env = process.env) {
33356
+ const raw = env.ZELARI_MISSION_MAX_TOKENS;
33357
+ if (!raw) return void 0;
33358
+ const n = Number.parseInt(raw, 10);
33359
+ return Number.isFinite(n) && n > 0 ? n : void 0;
33360
+ }
33159
33361
  function isMissionAutoStart(env = process.env) {
33160
33362
  return env.ZELARI_MISSION_AUTO === "1";
33161
33363
  }
33162
33364
  async function writeMissionState(projectRoot, state2) {
33163
- const dir = path29.join(projectRoot, ".zelari");
33164
- await fs17.mkdir(dir, { recursive: true });
33165
- await fs17.writeFile(
33166
- path29.join(dir, "mission-state.json"),
33365
+ const dir = path30.join(projectRoot, ".zelari");
33366
+ await fs18.mkdir(dir, { recursive: true });
33367
+ await fs18.writeFile(
33368
+ path30.join(dir, "mission-state.json"),
33167
33369
  JSON.stringify(state2, null, 2) + "\n",
33168
33370
  "utf8"
33169
33371
  );
33372
+ if (state2.trace?.length) {
33373
+ try {
33374
+ await saveTrace(projectRoot, state2.missionId, state2.trace);
33375
+ } catch {
33376
+ }
33377
+ }
33170
33378
  }
33171
33379
  function buildSlicePrompt(brief, userMessage, runMode, iteration) {
33172
33380
  if (runMode === "design-phase") {
@@ -33204,6 +33412,8 @@ async function runZelariMission(userMessage, brief, deps) {
33204
33412
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
33205
33413
  const maxIter = deps.maxIterations ?? resolveMaxIterations(deps.env);
33206
33414
  const maxStall = resolveMaxStall(deps.env);
33415
+ const maxCost = resolveMaxCost(deps.env);
33416
+ const maxTokens = resolveMaxTokens(deps.env);
33207
33417
  const missionId = deps.missionId ?? `m_${randomUUID5().slice(0, 8)}`;
33208
33418
  const startedAt = now().toISOString();
33209
33419
  const state2 = {
@@ -33239,6 +33449,8 @@ async function runZelariMission(userMessage, brief, deps) {
33239
33449
  let step = 0;
33240
33450
  let implStep = 0;
33241
33451
  let pendingDesign = designFirst;
33452
+ let cumulativeCostUsd = 0;
33453
+ let cumulativeTokens = 0;
33242
33454
  while (true) {
33243
33455
  const runMode = pendingDesign ? "design-phase" : "implementation";
33244
33456
  if (runMode === "implementation") {
@@ -33255,6 +33467,8 @@ async function runZelariMission(userMessage, brief, deps) {
33255
33467
  const promptIter = runMode === "implementation" ? implStep : 1;
33256
33468
  const slicePrompt = buildSlicePrompt(brief, userMessage, runMode, promptIter);
33257
33469
  const implementerRetry = runMode === "implementation" && implStep > 1;
33470
+ const sliceStartedAt = now().toISOString();
33471
+ const sliceStartMs = now().getTime();
33258
33472
  if (runMode === "design-phase") {
33259
33473
  deps.emit(
33260
33474
  `[zelari] design-phase (fuori budget) \xB7 step ${step} \xB7 slice ${brief.sliceMvp.id}`
@@ -33291,6 +33505,8 @@ async function runZelariMission(userMessage, brief, deps) {
33291
33505
  );
33292
33506
  return state2;
33293
33507
  }
33508
+ if (typeof result.costUsd === "number") cumulativeCostUsd += result.costUsd;
33509
+ if (typeof result.costTokens === "number") cumulativeTokens += result.costTokens;
33294
33510
  await deps.memory.add(
33295
33511
  JSON.stringify({
33296
33512
  iteration: step,
@@ -33316,6 +33532,20 @@ async function runZelariMission(userMessage, brief, deps) {
33316
33532
  }
33317
33533
  state2.lastCompletionOk = completionOk;
33318
33534
  state2.updatedAt = now().toISOString();
33535
+ state2.cumulativeCostUsd = cumulativeCostUsd;
33536
+ state2.cumulativeTokens = cumulativeTokens;
33537
+ if (!state2.trace) state2.trace = [];
33538
+ state2.trace.push({
33539
+ sliceId: brief.sliceMvp.id,
33540
+ iteration: step,
33541
+ runMode,
33542
+ completionOk,
33543
+ degraded: result.degraded,
33544
+ costTokens: typeof result.costTokens === "number" ? result.costTokens : void 0,
33545
+ costUsd: typeof result.costUsd === "number" ? result.costUsd : void 0,
33546
+ startedAt: sliceStartedAt,
33547
+ durationMs: now().getTime() - sliceStartMs
33548
+ });
33319
33549
  if (runMode === "design-phase") {
33320
33550
  pendingDesign = false;
33321
33551
  await writeMissionState(deps.projectRoot, state2);
@@ -33379,6 +33609,16 @@ async function runZelariMission(userMessage, brief, deps) {
33379
33609
  return state2;
33380
33610
  }
33381
33611
  }
33612
+ if (maxCost !== void 0 && cumulativeCostUsd >= maxCost || maxTokens !== void 0 && cumulativeTokens >= maxTokens) {
33613
+ state2.status = "stopped";
33614
+ state2.updatedAt = now().toISOString();
33615
+ await writeMissionState(deps.projectRoot, state2);
33616
+ const reason = maxCost !== void 0 && cumulativeCostUsd >= maxCost ? `budget USD ${formatCost(cumulativeCostUsd)} \u2265 ${formatCost(maxCost)}` : `token ${formatTokens(cumulativeTokens)} \u2265 ${formatTokens(maxTokens)}`;
33617
+ deps.emit(
33618
+ `[zelari] fermata: ${reason}. Imposta ZELARI_MISSION_MAX_COST / ZELARI_MISSION_MAX_TOKENS pi\xF9 alto, o usa un modello pi\xF9 economico. Stato salvato in .zelari/mission-state.json`
33619
+ );
33620
+ return state2;
33621
+ }
33382
33622
  await writeMissionState(deps.projectRoot, state2);
33383
33623
  }
33384
33624
  state2.status = "stopped";
@@ -33397,6 +33637,8 @@ var init_zelariMission = __esm({
33397
33637
  init_checkpointManager();
33398
33638
  init_fileStateStore();
33399
33639
  init_commitHelpers();
33640
+ init_modelPricing();
33641
+ init_traceStore();
33400
33642
  DEFAULT_MAX_ITER = 6;
33401
33643
  DEFAULT_MAX_STALL = 2;
33402
33644
  }
@@ -33554,6 +33796,8 @@ async function runAgentMissionSlice(deps) {
33554
33796
  });
33555
33797
  let text = "";
33556
33798
  let errored2 = false;
33799
+ let promptTokens = 0;
33800
+ let completionTokens = 0;
33557
33801
  try {
33558
33802
  for await (const event of harness.run()) {
33559
33803
  counter.onEvent(event);
@@ -33564,6 +33808,11 @@ async function runAgentMissionSlice(deps) {
33564
33808
  if (event.type === "agent_end" && event.reason === "error") {
33565
33809
  errored2 = true;
33566
33810
  }
33811
+ if (event.type === "message_end" && event.usage) {
33812
+ const u = event.usage;
33813
+ promptTokens += u.promptTokens ?? 0;
33814
+ completionTokens += u.completionTokens ?? 0;
33815
+ }
33567
33816
  if (event.type === "error" && event.severity === "fatal") {
33568
33817
  errored2 = true;
33569
33818
  }
@@ -33586,7 +33835,9 @@ async function runAgentMissionSlice(deps) {
33586
33835
  successfulWrites: counter.state.successfulWrites,
33587
33836
  emittedWrites: counter.state.emittedWrites,
33588
33837
  errored: errored2,
33589
- messages: harness.getMessages()
33838
+ messages: harness.getMessages(),
33839
+ promptTokens,
33840
+ completionTokens
33590
33841
  };
33591
33842
  }
33592
33843
  const initial = [
@@ -33598,6 +33849,8 @@ async function runAgentMissionSlice(deps) {
33598
33849
  let totalEmitted = pass.emittedWrites;
33599
33850
  let synthesisText = pass.text;
33600
33851
  let errored = pass.errored;
33852
+ let totalPromptTokens = pass.promptTokens;
33853
+ let totalCompletionTokens = pass.completionTokens;
33601
33854
  if (writeRetry && totalWrites === 0 && !errored) {
33602
33855
  deps.emit?.(
33603
33856
  "[zelari] build@agent: 0 write \u2014 forcing implementation retry"
@@ -33617,6 +33870,8 @@ async function runAgentMissionSlice(deps) {
33617
33870
  ${retry.text}` : retry.text;
33618
33871
  }
33619
33872
  errored = errored || retry.errored;
33873
+ totalPromptTokens += retry.promptTokens;
33874
+ totalCompletionTokens += retry.completionTokens;
33620
33875
  }
33621
33876
  const cleaned = cleanAgentContent(synthesisText, {
33622
33877
  stripQuestion: false,
@@ -33663,12 +33918,16 @@ ${retry.text}` : retry.text;
33663
33918
  else degraded = true;
33664
33919
  }
33665
33920
  void totalEmitted;
33921
+ const costTokens = totalPromptTokens + totalCompletionTokens;
33922
+ const costUsd = calculateCost(deps.model, totalPromptTokens, totalCompletionTokens);
33666
33923
  return {
33667
33924
  completionOk,
33668
33925
  ran: true,
33669
33926
  synthesisText: cleaned || void 0,
33670
33927
  writeCount: totalWrites,
33671
- degraded
33928
+ degraded,
33929
+ costTokens,
33930
+ costUsd
33672
33931
  };
33673
33932
  }
33674
33933
  var MUTATING, AGENT_MISSION_IMPLEMENTER_PREAMBLE;
@@ -33681,6 +33940,7 @@ var init_missionSlice = __esm({
33681
33940
  init_dist();
33682
33941
  init_buildPolicy();
33683
33942
  init_envNumber();
33943
+ init_modelPricing();
33684
33944
  MUTATING = /* @__PURE__ */ new Set(["write_file", "edit_file", "apply_diff"]);
33685
33945
  AGENT_MISSION_IMPLEMENTER_PREAMBLE = "You are the sole implementer for this Zelari mission slice. A multi-agent council may already have produced a plan under `.zelari/` \u2014 treat it as a SPEC to apply on disk. You MUST create or modify real project files with write_file / edit_file. Prose without successful writes is a failed slice.";
33686
33946
  }
@@ -34070,10 +34330,10 @@ var init_prereqChecks = __esm({
34070
34330
 
34071
34331
  // src/cli/plugins/prefs.ts
34072
34332
  import { existsSync as existsSync33, readFileSync as readFileSync28, writeFileSync as writeFileSync18, mkdirSync as mkdirSync13 } from "node:fs";
34073
- import path31 from "node:path";
34333
+ import path32 from "node:path";
34074
34334
  import os9 from "node:os";
34075
34335
  function getPluginPrefsPath() {
34076
- return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path31.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
34336
+ return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path32.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
34077
34337
  }
34078
34338
  function getPluginPrefs() {
34079
34339
  const file2 = getPluginPrefsPath();
@@ -34094,7 +34354,7 @@ function getPluginPrefs() {
34094
34354
  }
34095
34355
  function writePluginPrefs(prefs) {
34096
34356
  const file2 = getPluginPrefsPath();
34097
- mkdirSync13(path31.dirname(file2), { recursive: true });
34357
+ mkdirSync13(path32.dirname(file2), { recursive: true });
34098
34358
  writeFileSync18(file2, JSON.stringify(prefs, null, 2), {
34099
34359
  encoding: "utf-8",
34100
34360
  mode: 384
@@ -34131,7 +34391,7 @@ __export(registry_exports, {
34131
34391
  isBinaryOnPath: () => isBinaryOnPath
34132
34392
  });
34133
34393
  import { existsSync as existsSync34 } from "node:fs";
34134
- import path32 from "node:path";
34394
+ import path33 from "node:path";
34135
34395
  function detectLocalBin(bin) {
34136
34396
  return (cwd) => {
34137
34397
  try {
@@ -34149,7 +34409,7 @@ function isBinaryOnPath(bin, opts = {}) {
34149
34409
  const platform = opts.platform ?? process.platform;
34150
34410
  const exists = opts.exists ?? existsSync34;
34151
34411
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
34152
- const pathMod = platform === "win32" ? path32.win32 : path32.posix;
34412
+ const pathMod = platform === "win32" ? path33.win32 : path33.posix;
34153
34413
  const sep = platform === "win32" ? ";" : ":";
34154
34414
  const dirs = pathEnv.split(sep).filter((d) => d.length > 0);
34155
34415
  const candidates = [bin];
@@ -34279,6 +34539,59 @@ var init_registry2 = __esm({
34279
34539
  }
34280
34540
  });
34281
34541
 
34542
+ // src/cli/triggerLock.ts
34543
+ var triggerLock_exports = {};
34544
+ __export(triggerLock_exports, {
34545
+ acquireLock: () => acquireLock,
34546
+ lockPath: () => lockPath,
34547
+ releaseLock: () => releaseLock
34548
+ });
34549
+ import { promises as fs23 } from "node:fs";
34550
+ import * as path38 from "node:path";
34551
+ function lockPath(projectRoot) {
34552
+ return path38.join(projectRoot, ".zelari", "trigger.lock");
34553
+ }
34554
+ function isPidAlive(pid) {
34555
+ try {
34556
+ process.kill(pid, 0);
34557
+ return true;
34558
+ } catch (err) {
34559
+ const code = err.code;
34560
+ return code === "EPERM";
34561
+ }
34562
+ }
34563
+ async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date()) {
34564
+ const lp = lockPath(projectRoot);
34565
+ const dir = path38.dirname(lp);
34566
+ await fs23.mkdir(dir, { recursive: true });
34567
+ try {
34568
+ const raw = await fs23.readFile(lp, "utf8");
34569
+ const existing = JSON.parse(raw);
34570
+ if (existing.pid && isPidAlive(existing.pid)) {
34571
+ return { acquired: false, heldBy: existing.pid, lockPath: lp };
34572
+ }
34573
+ } catch {
34574
+ }
34575
+ const payload = {
34576
+ pid: process.pid,
34577
+ acquiredAt: now().toISOString()
34578
+ };
34579
+ await fs23.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
34580
+ return { acquired: true, lockPath: lp };
34581
+ }
34582
+ async function releaseLock(projectRoot) {
34583
+ const lp = lockPath(projectRoot);
34584
+ try {
34585
+ await fs23.unlink(lp);
34586
+ } catch {
34587
+ }
34588
+ }
34589
+ var init_triggerLock = __esm({
34590
+ "src/cli/triggerLock.ts"() {
34591
+ "use strict";
34592
+ }
34593
+ });
34594
+
34282
34595
  // src/cli/utils/doctor.ts
34283
34596
  var doctor_exports = {};
34284
34597
  __export(doctor_exports, {
@@ -34288,11 +34601,11 @@ import { execSync as execSync2 } from "node:child_process";
34288
34601
  import { existsSync as existsSync38, readFileSync as readFileSync31, readlinkSync, statSync as statSync6 } from "node:fs";
34289
34602
  import { createRequire as createRequire3 } from "node:module";
34290
34603
  import { fileURLToPath as fileURLToPath2 } from "node:url";
34291
- import path37 from "node:path";
34604
+ import path39 from "node:path";
34292
34605
  function findPackageRoot(start) {
34293
34606
  let dir = start;
34294
34607
  for (let i = 0; i < 6; i += 1) {
34295
- const candidate = path37.join(dir, "package.json");
34608
+ const candidate = path39.join(dir, "package.json");
34296
34609
  if (existsSync38(candidate)) {
34297
34610
  try {
34298
34611
  const pkg = JSON.parse(readFileSync31(candidate, "utf8"));
@@ -34300,11 +34613,11 @@ function findPackageRoot(start) {
34300
34613
  } catch {
34301
34614
  }
34302
34615
  }
34303
- const parent = path37.dirname(dir);
34616
+ const parent = path39.dirname(dir);
34304
34617
  if (parent === dir) break;
34305
34618
  dir = parent;
34306
34619
  }
34307
- return path37.resolve(__dirname3, "..", "..", "..");
34620
+ return path39.resolve(__dirname3, "..", "..", "..");
34308
34621
  }
34309
34622
  function tryExec(cmd) {
34310
34623
  try {
@@ -34318,7 +34631,7 @@ function tryExec(cmd) {
34318
34631
  }
34319
34632
  function readPackageJson3() {
34320
34633
  try {
34321
- const pkgPath = path37.join(packageRoot, "package.json");
34634
+ const pkgPath = path39.join(packageRoot, "package.json");
34322
34635
  return JSON.parse(readFileSync31(pkgPath, "utf8"));
34323
34636
  } catch {
34324
34637
  return null;
@@ -34334,7 +34647,7 @@ function checkShim(pkgName) {
34334
34647
  }
34335
34648
  const isWin = process.platform === "win32";
34336
34649
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
34337
- const shimPath = path37.join(prefix, shimName);
34650
+ const shimPath = path39.join(prefix, shimName);
34338
34651
  if (!existsSync38(shimPath)) {
34339
34652
  return FAIL(
34340
34653
  `shim not found at ${shimPath}
@@ -34362,8 +34675,8 @@ function checkShim(pkgName) {
34362
34675
  fix: npm install -g ${pkgName}@latest --force`
34363
34676
  );
34364
34677
  }
34365
- const resolved = path37.resolve(path37.dirname(shimPath), target);
34366
- const expected = path37.join(
34678
+ const resolved = path39.resolve(path39.dirname(shimPath), target);
34679
+ const expected = path39.join(
34367
34680
  prefix,
34368
34681
  "node_modules",
34369
34682
  pkgName,
@@ -34402,7 +34715,7 @@ function checkNode(pkg) {
34402
34715
  return OK(`node ${raw}`);
34403
34716
  }
34404
34717
  function checkBundle() {
34405
- const bundle = path37.join(packageRoot, "dist", "cli", "main.bundled.js");
34718
+ const bundle = path39.join(packageRoot, "dist", "cli", "main.bundled.js");
34406
34719
  if (!existsSync38(bundle)) {
34407
34720
  return FAIL(
34408
34721
  `dist/cli/main.bundled.js missing at ${bundle}
@@ -34423,7 +34736,7 @@ function checkRuntimeDeps() {
34423
34736
  const missing = [];
34424
34737
  for (const dep of required2) {
34425
34738
  try {
34426
- const localReq = createRequire3(path37.join(packageRoot, "package.json"));
34739
+ const localReq = createRequire3(path39.join(packageRoot, "package.json"));
34427
34740
  localReq.resolve(dep);
34428
34741
  } catch {
34429
34742
  missing.push(dep);
@@ -34599,7 +34912,7 @@ var init_doctor = __esm({
34599
34912
  "use strict";
34600
34913
  init_prereqChecks();
34601
34914
  require3 = createRequire3(import.meta.url);
34602
- __dirname3 = path37.dirname(fileURLToPath2(import.meta.url));
34915
+ __dirname3 = path39.dirname(fileURLToPath2(import.meta.url));
34603
34916
  packageRoot = findPackageRoot(__dirname3);
34604
34917
  OK = (message) => ({
34605
34918
  ok: true,
@@ -35207,83 +35520,7 @@ function StreamingTail(m) {
35207
35520
  // src/cli/components/StatusBar.tsx
35208
35521
  import React6 from "react";
35209
35522
  import { Box as Box5, Text as Text6 } from "ink";
35210
-
35211
- // src/cli/modelPricing.ts
35212
- var PRICES_PER_MILLION = {
35213
- // xAI Grok (list prices; grok-4.5 default reasoning_effort is "high")
35214
- "grok-4.5": { input: 2, output: 6, cachedInput: 0.5 },
35215
- "grok-4.3": { input: 1.25, output: 2.5, cachedInput: 0.2 },
35216
- "grok-4": { input: 3, output: 15 },
35217
- "grok-4-fast": { input: 0.2, output: 0.5 },
35218
- "grok-3": { input: 3, output: 15 },
35219
- "grok-3-mini": { input: 0.3, output: 0.5 },
35220
- "grok-2-vision": { input: 2, output: 10 },
35221
- // GLM / Z.AI
35222
- "glm-4.6": { input: 0.6, output: 2.2 },
35223
- "glm-4.5": { input: 0.5, output: 2 },
35224
- "glm-4.5-air": { input: 0.1, output: 0.6 },
35225
- "glm-z1": { input: 0.5, output: 2 },
35226
- // MiniMax
35227
- "MiniMax-M2.5": { input: 0.2, output: 1.1 },
35228
- "MiniMax-M2": { input: 0.2, output: 1.1 },
35229
- "MiniMax-M2-her": { input: 0.3, output: 1.2 },
35230
- // DeepSeek (global platform) — estimated list prices; override via
35231
- // ANATHEMA_PRICE_DEEPSEEK_V4_FLASH / ANATHEMA_PRICE_DEEPSEEK_V4_PRO.
35232
- // DeepSeek prompt-cache hits are ~10× cheaper than a cache miss.
35233
- "deepseek-v4-flash": { input: 0.14, output: 0.28, cachedInput: 0.014 },
35234
- "deepseek-v4-pro": { input: 0.55, output: 2.19, cachedInput: 0.055 },
35235
- // OpenAI (for openai-compatible fallback)
35236
- "gpt-4o": { input: 2.5, output: 10 },
35237
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
35238
- "gpt-4-turbo": { input: 10, output: 30 },
35239
- "o1-preview": { input: 15, output: 60 },
35240
- "o1-mini": { input: 3, output: 12 }
35241
- };
35242
- var DEFAULT_RATE = { input: 1, output: 3 };
35243
- var DEFAULT_CACHE_DISCOUNT = 0.25;
35244
- function envOverride(model) {
35245
- const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
35246
- const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
35247
- return parseRateOverride(raw);
35248
- }
35249
- function parseRateOverride(raw) {
35250
- if (!raw) return null;
35251
- const [inStr, outStr] = raw.split("/");
35252
- const input = Number.parseFloat(inStr);
35253
- const output = outStr !== void 0 ? Number.parseFloat(outStr) : input;
35254
- if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
35255
- return { input, output };
35256
- }
35257
- function getModelRate(model) {
35258
- if (!model) return DEFAULT_RATE;
35259
- return envOverride(model) ?? PRICES_PER_MILLION[model] ?? DEFAULT_RATE;
35260
- }
35261
- function calculateCost(model, promptTokens, completionTokens, cachedPromptTokens = 0) {
35262
- if (!Number.isFinite(promptTokens) || promptTokens < 0) return 0;
35263
- if (!Number.isFinite(completionTokens) || completionTokens < 0) return 0;
35264
- const rate = getModelRate(model);
35265
- const cached2 = Number.isFinite(cachedPromptTokens) && cachedPromptTokens > 0 ? Math.min(cachedPromptTokens, promptTokens) : 0;
35266
- const uncachedPrompt = promptTokens - cached2;
35267
- const cachedRate = rate.cachedInput ?? rate.input * DEFAULT_CACHE_DISCOUNT;
35268
- const inputCost = uncachedPrompt / 1e6 * rate.input + cached2 / 1e6 * cachedRate;
35269
- const outputCost = completionTokens / 1e6 * rate.output;
35270
- return Number((inputCost + outputCost).toFixed(6));
35271
- }
35272
- function formatCost(usd) {
35273
- if (!Number.isFinite(usd) || usd < 0) return "$0.0000";
35274
- if (usd === 0) return "$0.0000";
35275
- if (usd < 1e-4) return "<$0.0001";
35276
- return `$${usd.toFixed(4)}`;
35277
- }
35278
- function formatTokens(tokens) {
35279
- if (!Number.isFinite(tokens) || tokens < 0) return "0";
35280
- if (tokens < 1e3) return `${tokens}`;
35281
- if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
35282
- if (tokens < 1e9) return `${(tokens / 1e6).toFixed(2)}M`;
35283
- return `${(tokens / 1e9).toFixed(2)}B`;
35284
- }
35285
-
35286
- // src/cli/components/StatusBar.tsx
35523
+ init_modelPricing();
35287
35524
  function StatusBar({
35288
35525
  model,
35289
35526
  provider,
@@ -38627,6 +38864,9 @@ function finalizeStreamingAssistant(setMessages) {
38627
38864
  // src/cli/hooks/useChatTurn.ts
38628
38865
  init_conversationContext();
38629
38866
 
38867
+ // src/cli/hooks/chatStats.ts
38868
+ init_modelPricing();
38869
+
38630
38870
  // src/cli/state/promptCacheStats.ts
38631
38871
  function emptyPromptCacheStats() {
38632
38872
  return {
@@ -39206,6 +39446,13 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
39206
39446
  `[budget] ${event.message}`,
39207
39447
  Date.now()
39208
39448
  );
39449
+ } else if (event.code === "assistant_text_loop") {
39450
+ appendSystem(
39451
+ setMessages,
39452
+ `[text-loop] ${event.message}
39453
+ \u2192 Next message tip: "Continue with tools only \u2014 inspect disk, one write_file, stop."`,
39454
+ Date.now()
39455
+ );
39209
39456
  } else {
39210
39457
  appendSystem(setMessages, `[error] ${event.message}`, Date.now());
39211
39458
  }
@@ -40819,7 +41066,7 @@ ${formatSkillList(availableSkills)}`
40819
41066
  // src/cli/gitOps.ts
40820
41067
  import { execFile as execFile3 } from "node:child_process";
40821
41068
  import { promisify as promisify2 } from "node:util";
40822
- import path30 from "node:path";
41069
+ import path31 from "node:path";
40823
41070
  var execFileAsync2 = promisify2(execFile3);
40824
41071
  async function git2(cwd, args) {
40825
41072
  try {
@@ -40865,7 +41112,7 @@ async function undoWorkingChanges(opts = {}) {
40865
41112
  };
40866
41113
  }
40867
41114
  function defaultProjectRoot() {
40868
- return path30.resolve(__dirname, "..", "..", "..");
41115
+ return path31.resolve(__dirname, "..", "..", "..");
40869
41116
  }
40870
41117
 
40871
41118
  // src/cli/slashHandlers/git.ts
@@ -41505,17 +41752,17 @@ ${result.output.split("\n").slice(-8).join("\n")}` : "";
41505
41752
  }
41506
41753
 
41507
41754
  // src/cli/slashHandlers/promoteMember.ts
41508
- import { promises as fs18 } from "node:fs";
41509
- import path33 from "node:path";
41755
+ import { promises as fs19 } from "node:fs";
41756
+ import path34 from "node:path";
41510
41757
  import os10 from "node:os";
41511
41758
  async function handlePromoteMember(ctx, memberId) {
41512
41759
  try {
41513
41760
  const { promoteMember: promoteMember2 } = await Promise.resolve().then(() => (init_council(), council_exports));
41514
41761
  const { skill, markdown } = promoteMember2(memberId);
41515
- const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path33.join(os10.homedir(), ".tmp", "zelari-code", "skills");
41516
- await fs18.mkdir(skillDir, { recursive: true });
41517
- const filePath = path33.join(skillDir, `${skill.id}.md`);
41518
- await fs18.writeFile(filePath, markdown, "utf8");
41762
+ const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path34.join(os10.homedir(), ".tmp", "zelari-code", "skills");
41763
+ await fs19.mkdir(skillDir, { recursive: true });
41764
+ const filePath = path34.join(skillDir, `${skill.id}.md`);
41765
+ await fs19.writeFile(filePath, markdown, "utf8");
41519
41766
  appendSystem(
41520
41767
  ctx.setMessages,
41521
41768
  `[promote-member] ${skill.name} (${memberId}) \u2192 ${filePath}
@@ -41531,25 +41778,25 @@ async function handlePromoteMember(ctx, memberId) {
41531
41778
  }
41532
41779
 
41533
41780
  // src/cli/branchManager.ts
41534
- import { promises as fs19, existsSync as existsSync35, readFileSync as readFileSync29, writeFileSync as writeFileSync19, mkdirSync as mkdirSync14, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
41535
- import path34 from "node:path";
41781
+ import { promises as fs20, existsSync as existsSync35, readFileSync as readFileSync29, writeFileSync as writeFileSync19, mkdirSync as mkdirSync14, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
41782
+ import path35 from "node:path";
41536
41783
  import os11 from "node:os";
41537
41784
  var META_FILENAME = "meta.json";
41538
41785
  var SESSIONS_SUBDIR = "sessions";
41539
41786
  function getBranchesBaseDir() {
41540
- return process.env.ANATHEMA_BRANCHES_DIR ?? path34.join(os11.homedir(), ".tmp", "zelari-code", "branches");
41787
+ return process.env.ANATHEMA_BRANCHES_DIR ?? path35.join(os11.homedir(), ".tmp", "zelari-code", "branches");
41541
41788
  }
41542
41789
  function getSessionsBaseDir() {
41543
- return process.env.ANATHEMA_SESSIONS_DIR ?? path34.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
41790
+ return process.env.ANATHEMA_SESSIONS_DIR ?? path35.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
41544
41791
  }
41545
41792
  function branchPathFor(name, baseDir) {
41546
- return path34.join(baseDir, name);
41793
+ return path35.join(baseDir, name);
41547
41794
  }
41548
41795
  function metaPathFor(name, baseDir) {
41549
- return path34.join(baseDir, name, META_FILENAME);
41796
+ return path35.join(baseDir, name, META_FILENAME);
41550
41797
  }
41551
41798
  function sessionsPathFor(name, baseDir) {
41552
- return path34.join(baseDir, name, SESSIONS_SUBDIR);
41799
+ return path35.join(baseDir, name, SESSIONS_SUBDIR);
41553
41800
  }
41554
41801
  function readBranchMeta(name, baseDir) {
41555
41802
  const metaPath = metaPathFor(name, baseDir);
@@ -41574,13 +41821,13 @@ function readBranchMeta(name, baseDir) {
41574
41821
  }
41575
41822
  function writeBranchMeta(name, baseDir, meta3) {
41576
41823
  const metaPath = metaPathFor(name, baseDir);
41577
- mkdirSync14(path34.dirname(metaPath), { recursive: true });
41824
+ mkdirSync14(path35.dirname(metaPath), { recursive: true });
41578
41825
  writeFileSync19(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
41579
41826
  }
41580
41827
  async function countSessions(name, baseDir) {
41581
41828
  const sessionsPath = sessionsPathFor(name, baseDir);
41582
41829
  try {
41583
- const entries = await fs19.readdir(sessionsPath);
41830
+ const entries = await fs20.readdir(sessionsPath);
41584
41831
  return entries.filter((e) => e.endsWith(".jsonl")).length;
41585
41832
  } catch (err) {
41586
41833
  if (err.code === "ENOENT") return 0;
@@ -41625,15 +41872,15 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
41625
41872
  if (branchExists(name, baseDir)) {
41626
41873
  throw new BranchAlreadyExistsError(name);
41627
41874
  }
41628
- const sourcePath = path34.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
41875
+ const sourcePath = path35.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
41629
41876
  if (!existsSync35(sourcePath)) {
41630
41877
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
41631
41878
  }
41632
41879
  const branchPath = branchPathFor(name, baseDir);
41633
41880
  const branchSessionsPath = sessionsPathFor(name, baseDir);
41634
41881
  mkdirSync14(branchSessionsPath, { recursive: true });
41635
- const destPath = path34.join(branchSessionsPath, `${fromSessionId}.jsonl`);
41636
- await fs19.copyFile(sourcePath, destPath);
41882
+ const destPath = path35.join(branchSessionsPath, `${fromSessionId}.jsonl`);
41883
+ await fs20.copyFile(sourcePath, destPath);
41637
41884
  const meta3 = {
41638
41885
  name,
41639
41886
  createdAt: Date.now(),
@@ -41651,7 +41898,7 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
41651
41898
  async function listBranches(baseDir = getBranchesBaseDir()) {
41652
41899
  let entries;
41653
41900
  try {
41654
- entries = await fs19.readdir(baseDir);
41901
+ entries = await fs20.readdir(baseDir);
41655
41902
  } catch (err) {
41656
41903
  if (err.code === "ENOENT") return [];
41657
41904
  throw err;
@@ -41732,26 +41979,26 @@ async function handleBranchCheckout(ctx, branchName) {
41732
41979
  }
41733
41980
 
41734
41981
  // src/cli/slashHandlers/workspace.ts
41735
- import { promises as fs20 } from "node:fs";
41736
- import path35 from "node:path";
41982
+ import { promises as fs21 } from "node:fs";
41983
+ import path36 from "node:path";
41737
41984
  async function handleWorkspaceShow(ctx, what) {
41738
41985
  try {
41739
- const zelari = path35.join(process.cwd(), ".zelari");
41986
+ const zelari = path36.join(process.cwd(), ".zelari");
41740
41987
  let content;
41741
41988
  switch (what) {
41742
41989
  case "plan": {
41743
- const planPath = path35.join(zelari, "plan.md");
41990
+ const planPath = path36.join(zelari, "plan.md");
41744
41991
  try {
41745
- content = await fs20.readFile(planPath, "utf-8");
41992
+ content = await fs21.readFile(planPath, "utf-8");
41746
41993
  } catch {
41747
41994
  content = "(no plan.md yet \u2014 run a council session first)";
41748
41995
  }
41749
41996
  break;
41750
41997
  }
41751
41998
  case "decisions": {
41752
- const decisionsDir = path35.join(zelari, "decisions");
41999
+ const decisionsDir = path36.join(zelari, "decisions");
41753
42000
  try {
41754
- const files = (await fs20.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
42001
+ const files = (await fs21.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
41755
42002
  if (files.length === 0) {
41756
42003
  content = "(no ADRs yet \u2014 invoke /council to generate some)";
41757
42004
  } else {
@@ -41759,7 +42006,7 @@ async function handleWorkspaceShow(ctx, what) {
41759
42006
  `];
41760
42007
  const { parseFrontmatter: parseFrontmatter2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
41761
42008
  for (const f of files) {
41762
- const raw = await fs20.readFile(path35.join(decisionsDir, f), "utf-8");
42009
+ const raw = await fs21.readFile(path36.join(decisionsDir, f), "utf-8");
41763
42010
  const { meta: meta3, body } = parseFrontmatter2(raw);
41764
42011
  const title = meta3.title ?? body.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? f;
41765
42012
  lines.push(`- **${f.replace(/\.md$/, "")}** [${meta3.status ?? "unknown"}] ${title}`);
@@ -41772,27 +42019,27 @@ async function handleWorkspaceShow(ctx, what) {
41772
42019
  break;
41773
42020
  }
41774
42021
  case "risks": {
41775
- const risksPath = path35.join(zelari, "risks.md");
42022
+ const risksPath = path36.join(zelari, "risks.md");
41776
42023
  try {
41777
- content = await fs20.readFile(risksPath, "utf-8");
42024
+ content = await fs21.readFile(risksPath, "utf-8");
41778
42025
  } catch {
41779
42026
  content = "(no risks.md yet)";
41780
42027
  }
41781
42028
  break;
41782
42029
  }
41783
42030
  case "agents": {
41784
- const agentsPath = path35.join(process.cwd(), "AGENTS.MD");
42031
+ const agentsPath = path36.join(process.cwd(), "AGENTS.MD");
41785
42032
  try {
41786
- content = await fs20.readFile(agentsPath, "utf-8");
42033
+ content = await fs21.readFile(agentsPath, "utf-8");
41787
42034
  } catch {
41788
42035
  content = "(no AGENTS.MD yet at project root \u2014 run `/workspace sync` after a council session)";
41789
42036
  }
41790
42037
  break;
41791
42038
  }
41792
42039
  case "docs": {
41793
- const docsDir = path35.join(zelari, "docs");
42040
+ const docsDir = path36.join(zelari, "docs");
41794
42041
  try {
41795
- const files = (await fs20.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
42042
+ const files = (await fs21.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
41796
42043
  content = files.length ? `# Docs (${files.length})
41797
42044
 
41798
42045
  ` + files.map((f) => `- ${f}`).join("\n") : "(no docs drafts yet)";
@@ -41832,8 +42079,8 @@ async function handleWorkspaceReset(ctx, force) {
41832
42079
  return;
41833
42080
  }
41834
42081
  try {
41835
- const target = path35.join(process.cwd(), ".zelari");
41836
- await fs20.rm(target, { recursive: true, force: true });
42082
+ const target = path36.join(process.cwd(), ".zelari");
42083
+ await fs21.rm(target, { recursive: true, force: true });
41837
42084
  appendSystem(ctx.setMessages, "[workspace] .zelari/ removed");
41838
42085
  } catch (err) {
41839
42086
  appendSystem(ctx.setMessages, `[workspace reset error] ${err instanceof Error ? err.message : String(err)}`);
@@ -42191,16 +42438,16 @@ function handleModelsRefresh(ctx) {
42191
42438
  }
42192
42439
 
42193
42440
  // src/cli/slashHandlers/skills.ts
42194
- import path36 from "node:path";
42441
+ import path37 from "node:path";
42195
42442
  import os12 from "node:os";
42196
42443
 
42197
42444
  // src/cli/skillHistory.ts
42198
- import { promises as fs21, existsSync as existsSync36, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync15 } from "node:fs";
42445
+ import { promises as fs22, existsSync as existsSync36, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync15 } from "node:fs";
42199
42446
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
42200
42447
  async function readSkillHistory(file2) {
42201
42448
  let raw = "";
42202
42449
  try {
42203
- raw = await fs21.readFile(file2, "utf-8");
42450
+ raw = await fs22.readFile(file2, "utf-8");
42204
42451
  } catch {
42205
42452
  return [];
42206
42453
  }
@@ -42293,7 +42540,7 @@ async function applySteerInterrupt(options) {
42293
42540
 
42294
42541
  // src/cli/slashHandlers/skills.ts
42295
42542
  async function handleSkillStats(ctx, skillId) {
42296
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path36.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
42543
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path37.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
42297
42544
  try {
42298
42545
  const records = await readSkillHistory(historyFile);
42299
42546
  const stats = getSkillStats(records, skillId);
@@ -42309,7 +42556,7 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
42309
42556
  appendSystem(ctx.setMessages, fallbackMessage ?? "[skill-compare] missing args");
42310
42557
  return;
42311
42558
  }
42312
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path36.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
42559
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path37.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
42313
42560
  try {
42314
42561
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
42315
42562
  appendSystem(ctx.setMessages, formatted);
@@ -43723,6 +43970,7 @@ function parseHeadlessFlags(argv) {
43723
43970
  let provider;
43724
43971
  let model;
43725
43972
  let history2;
43973
+ let once = false;
43726
43974
  for (let i = 0; i < argv.length; i++) {
43727
43975
  const arg = argv[i];
43728
43976
  if (arg === "--headless") continue;
@@ -43811,6 +44059,8 @@ function parseHeadlessFlags(argv) {
43811
44059
  }
43812
44060
  i++;
43813
44061
  }
44062
+ } else if (arg === "--once") {
44063
+ once = true;
43814
44064
  }
43815
44065
  }
43816
44066
  if (councilFlag && !modeExplicit) {
@@ -43833,7 +44083,8 @@ function parseHeadlessFlags(argv) {
43833
44083
  useCouncil: mode === "council",
43834
44084
  provider,
43835
44085
  model,
43836
- ...history2 && history2.length > 0 ? { history: history2 } : {}
44086
+ ...history2 && history2.length > 0 ? { history: history2 } : {},
44087
+ ...once ? { once: true } : {}
43837
44088
  }
43838
44089
  };
43839
44090
  }
@@ -44516,6 +44767,22 @@ ${JSON.stringify({ deliverable: brief.deliverableThisMission, mvp: brief.sliceMv
44516
44767
  }
44517
44768
  let exitCode = 0;
44518
44769
  let lastMissionAssistant = "";
44770
+ let lockAcquired = false;
44771
+ if (opts.once) {
44772
+ const { acquireLock: acquireLock2 } = await Promise.resolve().then(() => (init_triggerLock(), triggerLock_exports));
44773
+ const lockRes = await acquireLock2(projectRoot);
44774
+ if (!lockRes.acquired) {
44775
+ process.stderr.write(
44776
+ `[zelari-code --once] skip: another mission is running (pid ${lockRes.heldBy}).
44777
+ `
44778
+ );
44779
+ return 0;
44780
+ }
44781
+ lockAcquired = true;
44782
+ if (!process.env.ZELARI_MISSION_MAX_ITER) {
44783
+ process.env.ZELARI_MISSION_MAX_ITER = "1";
44784
+ }
44785
+ }
44519
44786
  try {
44520
44787
  const state2 = await runZelariMission2(missionTask, brief, {
44521
44788
  projectRoot,
@@ -44734,6 +45001,10 @@ ${ragContext}` : slicePrompt;
44734
45001
  );
44735
45002
  return 2;
44736
45003
  } finally {
45004
+ if (lockAcquired) {
45005
+ const { releaseLock: releaseLock2 } = await Promise.resolve().then(() => (init_triggerLock(), triggerLock_exports));
45006
+ await releaseLock2(projectRoot);
45007
+ }
44737
45008
  await memory.close().catch(() => void 0);
44738
45009
  }
44739
45010
  if (opts.output === "json") {