zelari-code 1.17.0 → 1.18.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.
@@ -18415,6 +18415,7 @@ Earlier members' outputs are shared context. Build on them; do not re-derive or
18415
18415
  - Pass complete, valid arguments. Required parameters must be present.
18416
18416
  - Prefer tools over asking the user to paste file contents.
18417
18417
  - After durable changes, briefly name what you created or modified.
18418
+ - **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.
18418
18419
  - Text-only tool blocks (\`---TOOLS---\` JSON) are a legacy fallback \u2014 use them only if the runtime has no native tool channel.`
18419
18420
  };
18420
18421
  CODING_PRACTICES_MODULE = {
@@ -18429,7 +18430,8 @@ Earlier members' outputs are shared context. Build on them; do not re-derive or
18429
18430
  - **Don't invent**: no fake APIs, deps, or config keys \u2014 discover from the tree or package manifests.
18430
18431
  - **Verify**: after non-trivial edits, run the project's tests/typecheck/build when available; fix failures you introduced.
18431
18432
  - **Use project scripts**: prefer package.json / Makefile / existing tooling over ad-hoc commands.
18432
- - **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.`
18433
+ - **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.
18434
+ - **No spam**: never repeat the same paragraph or status line. One diagnosis, then tools (or one short final answer).`
18433
18435
  };
18434
18436
  CLARIFICATION_PROTOCOL_MODULE = {
18435
18437
  type: "custom",
@@ -19124,6 +19126,159 @@ var init_events = __esm({
19124
19126
  }
19125
19127
  });
19126
19128
 
19129
+ // packages/core/dist/core/textLoopDetect.js
19130
+ function normalizeLoopUnit(s) {
19131
+ return s.replace(/<\/?small>/gi, "").replace(/<\/?(?:p|div|span|b|i|em|strong|br)\b[^>]*>/gi, "").replace(/\s+/g, " ").trim();
19132
+ }
19133
+ function detectChunkSequenceLoop(chunks, kind) {
19134
+ if (chunks.length < MIN_REPEATS)
19135
+ return { looping: false };
19136
+ const maxK = Math.min(12, Math.floor(chunks.length / MIN_REPEATS));
19137
+ for (let k = 1; k <= maxK; k++) {
19138
+ const unitParts = chunks.slice(chunks.length - k);
19139
+ const unitKey = unitParts.join("\n");
19140
+ if (normalizeLoopUnit(unitKey).length < MIN_UNIT)
19141
+ continue;
19142
+ let count = 1;
19143
+ let pos = chunks.length - k;
19144
+ while (pos - k >= 0) {
19145
+ const prev2 = chunks.slice(pos - k, pos);
19146
+ let same = true;
19147
+ for (let i = 0; i < k; i++) {
19148
+ if (prev2[i] !== unitParts[i]) {
19149
+ same = false;
19150
+ break;
19151
+ }
19152
+ }
19153
+ if (!same)
19154
+ break;
19155
+ count++;
19156
+ pos -= k;
19157
+ }
19158
+ if (count >= MIN_REPEATS) {
19159
+ return {
19160
+ looping: true,
19161
+ unit: unitKey,
19162
+ count,
19163
+ kind
19164
+ };
19165
+ }
19166
+ }
19167
+ return { looping: false };
19168
+ }
19169
+ function detectSuffixPeriod(text) {
19170
+ const compact = text.replace(/\s+/g, " ").trim();
19171
+ if (compact.length < MIN_UNIT * MIN_REPEATS)
19172
+ return { looping: false };
19173
+ const tail = compact.slice(-Math.min(compact.length, MAX_TAIL));
19174
+ const maxP = Math.min(MAX_PERIOD, Math.floor(tail.length / MIN_REPEATS));
19175
+ for (let period = MIN_UNIT; period <= maxP; period++) {
19176
+ const need = period * MIN_REPEATS;
19177
+ if (tail.length < need)
19178
+ continue;
19179
+ const unit = tail.slice(tail.length - period);
19180
+ if (normalizeLoopUnit(unit).length < MIN_UNIT * 0.55)
19181
+ continue;
19182
+ let ok = true;
19183
+ for (let r = 1; r < MIN_REPEATS; r++) {
19184
+ const start = tail.length - period * (r + 1);
19185
+ if (tail.slice(start, start + period) !== unit) {
19186
+ ok = false;
19187
+ break;
19188
+ }
19189
+ }
19190
+ if (!ok)
19191
+ continue;
19192
+ let count = MIN_REPEATS;
19193
+ let end = tail.length - period * MIN_REPEATS;
19194
+ while (end >= period && tail.slice(end - period, end) === unit) {
19195
+ count++;
19196
+ end -= period;
19197
+ }
19198
+ return {
19199
+ looping: true,
19200
+ unit,
19201
+ count,
19202
+ kind: "suffix"
19203
+ };
19204
+ }
19205
+ return { looping: false };
19206
+ }
19207
+ function detectAssistantTextLoop(text) {
19208
+ if (!text || text.length < MIN_UNIT * MIN_REPEATS) {
19209
+ return { looping: false };
19210
+ }
19211
+ const nl = text.replace(/\r\n/g, "\n");
19212
+ const paragraphs = nl.split(/\n\s*\n+/).map((p3) => normalizeLoopUnit(p3)).filter((p3) => p3.length > 0);
19213
+ const paraHit = detectChunkSequenceLoop(paragraphs, "paragraph");
19214
+ if (paraHit.looping)
19215
+ return paraHit;
19216
+ const lines = nl.split("\n").map((l) => normalizeLoopUnit(l)).filter((l) => l.length > 0);
19217
+ const lineHit = detectChunkSequenceLoop(lines, "line");
19218
+ if (lineHit.looping)
19219
+ return lineHit;
19220
+ return detectSuffixPeriod(nl);
19221
+ }
19222
+ function collapseLoopedAssistantText(text) {
19223
+ const hit = detectAssistantTextLoop(text);
19224
+ if (!hit.looping)
19225
+ return text;
19226
+ const note = `
19227
+
19228
+ [system: stopped repeating the same text \xD7${hit.count}; call tools or finish.]`;
19229
+ if (hit.kind === "paragraph" || hit.kind === "line") {
19230
+ const joinSep = hit.kind === "paragraph" ? "\n\n" : "\n";
19231
+ const unitParts = hit.unit.split("\n").map((p3) => normalizeLoopUnit(p3));
19232
+ const k = unitParts.length;
19233
+ if (k === 0)
19234
+ return text + note;
19235
+ const rawParts = text.replace(/\r\n/g, "\n").split(hit.kind === "paragraph" ? /\n\s*\n+/ : /\n/).filter((p3) => normalizeLoopUnit(p3).length > 0);
19236
+ if (rawParts.length < k * MIN_REPEATS) {
19237
+ return collapseByCharBudget(text, hit.unit, hit.count) + note;
19238
+ }
19239
+ let trailBlocks = 0;
19240
+ let pos = rawParts.length;
19241
+ while (pos >= k) {
19242
+ let same = true;
19243
+ for (let i = 0; i < k; i++) {
19244
+ if (normalizeLoopUnit(rawParts[pos - k + i]) !== unitParts[i]) {
19245
+ same = false;
19246
+ break;
19247
+ }
19248
+ }
19249
+ if (!same)
19250
+ break;
19251
+ trailBlocks++;
19252
+ pos -= k;
19253
+ }
19254
+ if (trailBlocks < MIN_REPEATS) {
19255
+ return collapseByCharBudget(text, hit.unit, hit.count) + note;
19256
+ }
19257
+ const keepBlocks = 2;
19258
+ const keepParts = rawParts.slice(0, rawParts.length - k * (trailBlocks - keepBlocks));
19259
+ return keepParts.join(joinSep).trimEnd() + note;
19260
+ }
19261
+ return collapseByCharBudget(text, hit.unit, hit.count) + note;
19262
+ }
19263
+ function collapseByCharBudget(text, unit, count) {
19264
+ const unitLen = Math.max(unit.length, MIN_UNIT);
19265
+ if (count < MIN_REPEATS)
19266
+ return text;
19267
+ const drop = unitLen * (count - 2);
19268
+ const cut = Math.max(0, text.length - drop);
19269
+ return text.slice(0, cut).trimEnd();
19270
+ }
19271
+ var MIN_UNIT, MIN_REPEATS, MAX_TAIL, MAX_PERIOD;
19272
+ var init_textLoopDetect = __esm({
19273
+ "packages/core/dist/core/textLoopDetect.js"() {
19274
+ "use strict";
19275
+ MIN_UNIT = 48;
19276
+ MIN_REPEATS = 3;
19277
+ MAX_TAIL = 6e3;
19278
+ MAX_PERIOD = 600;
19279
+ }
19280
+ });
19281
+
19127
19282
  // packages/core/dist/core/AgentHarness.js
19128
19283
  function hashToolCall(toolName, args) {
19129
19284
  const canonical = stableStringify(args);
@@ -19362,6 +19517,8 @@ var init_AgentHarness = __esm({
19362
19517
  "packages/core/dist/core/AgentHarness.js"() {
19363
19518
  "use strict";
19364
19519
  init_events();
19520
+ init_textLoopDetect();
19521
+ init_textLoopDetect();
19365
19522
  AgentHarness = class {
19366
19523
  config;
19367
19524
  eventBus;
@@ -19811,6 +19968,7 @@ ${shared2.content}`,
19811
19968
  const turnToolCalls = [];
19812
19969
  const turnToolResults = [];
19813
19970
  const pendingNativeTools = [];
19971
+ let lastLoopCheckLen = 0;
19814
19972
  for await (const delta of stream) {
19815
19973
  if (this.cancelled) {
19816
19974
  const cancelEvent = createBrainEvent("error", this.sessionId, {
@@ -19827,6 +19985,29 @@ ${shared2.content}`,
19827
19985
  const deltaEvent = createBrainEvent("message_delta", this.sessionId, { messageId, delta: delta.delta, ...this.memberFields() });
19828
19986
  this.emit(deltaEvent);
19829
19987
  yield deltaEvent;
19988
+ if (turnText.length >= 48 * 3 && turnText.length - lastLoopCheckLen >= 48) {
19989
+ lastLoopCheckLen = turnText.length;
19990
+ const loopHit = detectAssistantTextLoop(turnText);
19991
+ if (loopHit.looping) {
19992
+ const loopErr = createBrainEvent("error", this.sessionId, {
19993
+ severity: "recoverable",
19994
+ 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.`,
19995
+ code: "assistant_text_loop"
19996
+ });
19997
+ this.emit(loopErr);
19998
+ yield loopErr;
19999
+ finishRef.value = "stop";
20000
+ const sealed = collapseLoopedAssistantText(turnText);
20001
+ if (sealed.length > 0 || turnReasoning.length > 0) {
20002
+ this.config.messages.push({
20003
+ role: "assistant",
20004
+ content: sealed,
20005
+ ...turnReasoning.length > 0 ? { reasoningContent: turnReasoning } : {}
20006
+ });
20007
+ }
20008
+ break;
20009
+ }
20010
+ }
19830
20011
  } else if (delta.kind === "thinking") {
19831
20012
  turnReasoning += delta.delta;
19832
20013
  const thinkEvent = createBrainEvent("thinking_delta", this.sessionId, {
@@ -20247,7 +20428,10 @@ var harness_exports = {};
20247
20428
  __export(harness_exports, {
20248
20429
  AgentHarness: () => AgentHarness,
20249
20430
  SessionJsonlWriter: () => SessionJsonlWriter,
20431
+ collapseLoopedAssistantText: () => collapseLoopedAssistantText,
20432
+ detectAssistantTextLoop: () => detectAssistantTextLoop,
20250
20433
  hashToolCall: () => hashToolCall,
20434
+ normalizeLoopUnit: () => normalizeLoopUnit,
20251
20435
  normalizeTextToolArgs: () => normalizeTextToolArgs,
20252
20436
  parseMinimaxStyleToolCalls: () => parseMinimaxStyleToolCalls,
20253
20437
  parseTextToolCalls: () => parseTextToolCalls,
@@ -22449,7 +22633,7 @@ async function runBrowserCheck(options, loader) {
22449
22633
  if (!pw) {
22450
22634
  return {
22451
22635
  ...base,
22452
- error: "browser automation unavailable \u2014 install Playwright (`npm i -D playwright && npx playwright install chromium`) to enable browser_check"
22636
+ error: "browser automation unavailable \u2014 Playwright is not installed in this workspace. Install it with: `zelari-code --plugins-install playwright --cwd .` (or Desktop banner \u201CInstall\u201D, or CLI `/plugins install playwright`, or `npm i -D playwright && npx playwright install chromium`). Then re-run browser_check."
22453
22637
  };
22454
22638
  }
22455
22639
  const timeout = options.timeoutMs ?? 15e3;
@@ -28622,7 +28806,7 @@ function composeProjectContext(input) {
28622
28806
  default: 3e3,
28623
28807
  min: 200
28624
28808
  });
28625
- const wantDurable = input.includeDurableState ?? (input.mode === "council" || input.mode === "zelari");
28809
+ const wantDurable = input.includeDurableState ?? (process.env.ZELARI_STATE !== "0" && (input.mode === "council" || input.mode === "zelari" || input.mode === "agent"));
28626
28810
  let durableRaw = input.durableState?.trim() ?? "";
28627
28811
  if (!durableRaw && wantDurable) {
28628
28812
  durableRaw = readDurableHeadSync(cwd);
@@ -28712,6 +28896,42 @@ var init_planDetect = __esm({
28712
28896
  }
28713
28897
  });
28714
28898
 
28899
+ // src/cli/state/loadDurableContext.ts
28900
+ var loadDurableContext_exports = {};
28901
+ __export(loadDurableContext_exports, {
28902
+ clearDurableContextCache: () => clearDurableContextCache,
28903
+ loadDurableContext: () => loadDurableContext
28904
+ });
28905
+ function clearDurableContextCache() {
28906
+ cache = null;
28907
+ }
28908
+ async function loadDurableContext(projectRoot, opts) {
28909
+ const env = opts?.env ?? process.env;
28910
+ if (!isStateEnabled(env)) return "";
28911
+ const cacheMs = opts?.cacheMs ?? DEFAULT_CACHE_MS;
28912
+ const now = Date.now();
28913
+ if (cache && cache.projectRoot === projectRoot && now - cache.at < cacheMs) {
28914
+ return cache.text;
28915
+ }
28916
+ try {
28917
+ const store = await getStateStore(projectRoot, env);
28918
+ const text = await store.materializeContext(void 0, opts?.maxChars);
28919
+ cache = { text: text || "", at: now, projectRoot };
28920
+ return cache.text;
28921
+ } catch {
28922
+ return "";
28923
+ }
28924
+ }
28925
+ var DEFAULT_CACHE_MS, cache;
28926
+ var init_loadDurableContext = __esm({
28927
+ "src/cli/state/loadDurableContext.ts"() {
28928
+ "use strict";
28929
+ init_fileStateStore();
28930
+ DEFAULT_CACHE_MS = 2e3;
28931
+ cache = null;
28932
+ }
28933
+ });
28934
+
28715
28935
  // src/cli/workspace/storage.ts
28716
28936
  var storage_exports = {};
28717
28937
  __export(storage_exports, {
@@ -31539,8 +31759,8 @@ __export(commitHelpers_exports, {
31539
31759
  async function tryStateCommit(args) {
31540
31760
  try {
31541
31761
  const store = args.store ?? await getStateStore(args.projectRoot, args.env);
31542
- let workspaceCheckpointId;
31543
- if (args.withCheckpoint && (args.env ?? process.env).ZELARI_CHECKPOINT !== "0") {
31762
+ let workspaceCheckpointId = args.workspaceCheckpointId;
31763
+ if (!workspaceCheckpointId && args.withCheckpoint && (args.env ?? process.env).ZELARI_CHECKPOINT !== "0") {
31544
31764
  const cp = await createCheckpoint(
31545
31765
  args.projectRoot,
31546
31766
  `state ${args.layer ?? args.label}`.slice(0, 80)
@@ -31556,9 +31776,15 @@ async function tryStateCommit(args) {
31556
31776
  verification: args.verification,
31557
31777
  changedPaths: args.changedPaths,
31558
31778
  discoveries: args.discoveries,
31779
+ stablePromptHash: args.stablePromptHash,
31559
31780
  force: args.force
31560
31781
  });
31561
31782
  if (!meta3.id) return { ok: false, error: "noop store", checkpointId: workspaceCheckpointId };
31783
+ try {
31784
+ const { clearDurableContextCache: clearDurableContextCache2 } = await Promise.resolve().then(() => (init_loadDurableContext(), loadDurableContext_exports));
31785
+ clearDurableContextCache2();
31786
+ } catch {
31787
+ }
31562
31788
  return { ok: true, meta: meta3, checkpointId: workspaceCheckpointId };
31563
31789
  } catch (err) {
31564
31790
  return {
@@ -31840,14 +32066,7 @@ async function runZelariMission(userMessage, brief, deps) {
31840
32066
  limit: 8,
31841
32067
  metadataFilter: { projectRoot: deps.projectRoot }
31842
32068
  });
31843
- let durableBlock = "";
31844
- try {
31845
- durableBlock = await stateStore.materializeContext();
31846
- } catch {
31847
- durableBlock = "";
31848
- }
31849
- const memBlock = formatMemoryHits(hits);
31850
- const ragContext = [durableBlock, memBlock].filter(Boolean).join("\n\n");
32069
+ const ragContext = formatMemoryHits(hits);
31851
32070
  const promptIter = runMode === "implementation" ? implStep : 1;
31852
32071
  const slicePrompt = buildSlicePrompt(brief, userMessage, runMode, promptIter);
31853
32072
  const implementerRetry = runMode === "implementation" && implStep > 1;
@@ -31919,7 +32138,9 @@ async function runZelariMission(userMessage, brief, deps) {
31919
32138
  sessionId: missionId,
31920
32139
  verification: { ok: hard, ran: result.ran },
31921
32140
  force: !hard,
31922
- withCheckpoint: hard,
32141
+ // Prefer linking the mission-start checkpoint (no explosion).
32142
+ workspaceCheckpointId: missionCheckpointId,
32143
+ withCheckpoint: hard && !missionCheckpointId,
31923
32144
  discoveries: discoveriesFromOutcome({
31924
32145
  stepId: `${missionId}-${step}`,
31925
32146
  synthesis: result.synthesisText,
@@ -31930,7 +32151,7 @@ async function runZelariMission(userMessage, brief, deps) {
31930
32151
  if (commitRes.ok && commitRes.meta?.id) {
31931
32152
  if (hard) {
31932
32153
  state2.lastGoodCommitId = commitRes.meta.id;
31933
- if (commitRes.checkpointId) {
32154
+ if (commitRes.checkpointId && !missionCheckpointId) {
31934
32155
  missionCheckpointId = commitRes.checkpointId;
31935
32156
  }
31936
32157
  }
@@ -36673,6 +36894,48 @@ function finalizeStreamingAssistant(setMessages) {
36673
36894
  // src/cli/hooks/useChatTurn.ts
36674
36895
  init_conversationContext();
36675
36896
 
36897
+ // src/cli/state/promptCacheStats.ts
36898
+ function emptyPromptCacheStats() {
36899
+ return {
36900
+ promptTokens: 0,
36901
+ cachedTokens: 0,
36902
+ premiumTokens: 0,
36903
+ hitRate: 0,
36904
+ estimatedCostUsd: 0,
36905
+ stableBustCount: 0,
36906
+ turns: 0
36907
+ };
36908
+ }
36909
+ function accumulatePromptCacheStats(prev2, turn) {
36910
+ const promptTokens = prev2.promptTokens + Math.max(0, turn.promptTokens);
36911
+ const cachedTokens = prev2.cachedTokens + Math.max(0, turn.cachedTokens);
36912
+ const premiumDelta = Math.max(0, turn.promptTokens - turn.cachedTokens);
36913
+ const premiumTokens = prev2.premiumTokens + premiumDelta;
36914
+ const hitRate = promptTokens > 0 ? cachedTokens / promptTokens : 0;
36915
+ let stableBustCount = prev2.stableBustCount;
36916
+ let lastStableHash = prev2.lastStableHash;
36917
+ if (turn.stableHash) {
36918
+ if (lastStableHash && lastStableHash !== turn.stableHash) {
36919
+ stableBustCount += 1;
36920
+ }
36921
+ lastStableHash = turn.stableHash;
36922
+ }
36923
+ return {
36924
+ promptTokens,
36925
+ cachedTokens,
36926
+ premiumTokens,
36927
+ hitRate,
36928
+ estimatedCostUsd: prev2.estimatedCostUsd + (turn.costUsd ?? 0),
36929
+ lastStableHash,
36930
+ stableBustCount,
36931
+ turns: prev2.turns + 1
36932
+ };
36933
+ }
36934
+ function formatCacheStatsLine(stats) {
36935
+ const pct = stats.promptTokens > 0 ? Math.round(stats.hitRate * 100) : 0;
36936
+ return `cache hit ${pct}% \xB7 premium ${stats.premiumTokens} \xB7 cached ${stats.cachedTokens} \xB7 stable busts ${stats.stableBustCount} \xB7 turns ${stats.turns}`;
36937
+ }
36938
+
36676
36939
  // src/cli/hooks/chatStats.ts
36677
36940
  function computeSessionStatsDelta(realUsage, userText, assistantContent, model, prev2, opts) {
36678
36941
  const promptTokens = realUsage ? realUsage.promptTokens : Math.ceil(userText.length / 4);
@@ -36680,31 +36943,41 @@ function computeSessionStatsDelta(realUsage, userText, assistantContent, model,
36680
36943
  const cachedPromptTokens = realUsage?.cachedPromptTokens ?? 0;
36681
36944
  const turnCost = calculateCost(model, promptTokens, completionTokens, cachedPromptTokens);
36682
36945
  const contextTokens = realUsage ? realUsage.totalTokens || promptTokens + completionTokens : promptTokens + completionTokens;
36683
- const nextCached = (prev2.cachedTokens ?? 0) + cachedPromptTokens;
36684
- const nextPrompt = (prev2.promptTokens ?? 0) + promptTokens;
36685
- const premiumDelta = Math.max(0, promptTokens - cachedPromptTokens);
36686
- const nextPremium = (prev2.premiumTokens ?? 0) + premiumDelta;
36687
- const cacheHitRate = nextPrompt > 0 ? nextCached / nextPrompt : 0;
36688
- let stableBustCount = prev2.stableBustCount ?? 0;
36689
- let lastStableHash = prev2.lastStableHash;
36690
- if (opts?.stableHash) {
36691
- if (lastStableHash && lastStableHash !== opts.stableHash) {
36692
- stableBustCount += 1;
36693
- }
36694
- lastStableHash = opts.stableHash;
36695
- }
36946
+ const prevCache = {
36947
+ ...emptyPromptCacheStats(),
36948
+ promptTokens: prev2.promptTokens ?? 0,
36949
+ cachedTokens: prev2.cachedTokens ?? 0,
36950
+ premiumTokens: prev2.premiumTokens ?? 0,
36951
+ hitRate: prev2.cacheHitRate ?? 0,
36952
+ estimatedCostUsd: prev2.totalCostUsd,
36953
+ lastStableHash: prev2.lastStableHash,
36954
+ stableBustCount: prev2.stableBustCount ?? 0,
36955
+ turns: 0
36956
+ };
36957
+ const nextCache = accumulatePromptCacheStats(prevCache, {
36958
+ promptTokens,
36959
+ cachedTokens: cachedPromptTokens,
36960
+ costUsd: turnCost,
36961
+ stableHash: opts?.stableHash
36962
+ });
36696
36963
  return {
36697
36964
  totalTokens: prev2.totalTokens + promptTokens + completionTokens,
36698
36965
  totalCostUsd: prev2.totalCostUsd + turnCost,
36699
- cachedTokens: nextCached,
36966
+ cachedTokens: nextCache.cachedTokens,
36700
36967
  contextTokens,
36701
- premiumTokens: nextPremium,
36702
- cacheHitRate,
36703
- promptTokens: nextPrompt,
36704
- lastStableHash,
36705
- stableBustCount
36968
+ premiumTokens: nextCache.premiumTokens,
36969
+ cacheHitRate: nextCache.hitRate,
36970
+ promptTokens: nextCache.promptTokens,
36971
+ lastStableHash: nextCache.lastStableHash,
36972
+ stableBustCount: nextCache.stableBustCount
36706
36973
  };
36707
36974
  }
36975
+ function resolvePromptCacheTtl(env = process.env) {
36976
+ const raw = (env.ZELARI_PROMPT_CACHE_TTL ?? "auto").toLowerCase().trim();
36977
+ if (raw === "1h" || raw === "1hour" || raw === "long") return "1h";
36978
+ if (raw === "5m" || raw === "5min" || raw === "short") return "5m";
36979
+ return "auto";
36980
+ }
36708
36981
 
36709
36982
  // src/cli/hooks/useChatTurn.ts
36710
36983
  init_envNumber();
@@ -36901,14 +37174,22 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
36901
37174
  const { composeProjectContext: composeProjectContext2 } = await Promise.resolve().then(() => (init_composeContext(), composeContext_exports));
36902
37175
  const { hasWorkspacePlan: hasWorkspacePlan2 } = await Promise.resolve().then(() => (init_planDetect(), planDetect_exports));
36903
37176
  hasPlan = hasWorkspacePlan2(cwd);
37177
+ const { loadDurableContext: loadDurableContext2 } = await Promise.resolve().then(() => (init_loadDurableContext(), loadDurableContext_exports));
37178
+ const durableState = await loadDurableContext2(cwd);
36904
37179
  const composed = composeProjectContext2({
36905
37180
  mode: "agent",
36906
37181
  cwd,
36907
37182
  userMessage: userText,
36908
- includeLessons: false
37183
+ includeLessons: false,
37184
+ durableState: durableState || void 0,
37185
+ // Pre-loaded async — skip sync fallback double-read.
37186
+ includeDurableState: false
36909
37187
  });
36910
37188
  composedWorkspace = composed.workspaceContext;
36911
37189
  composedInstructions = composed.projectInstructions;
37190
+ if (composed.ragContext) {
37191
+ composedWorkspace = [composedWorkspace, composed.ragContext].filter(Boolean).join("\n\n");
37192
+ }
36912
37193
  for (const w of composed.warnings) {
36913
37194
  appendSystem(setMessages, w, Date.now());
36914
37195
  }
@@ -37514,6 +37795,35 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
37514
37795
  let sliceRan = false;
37515
37796
  let sliceDegraded = false;
37516
37797
  const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
37798
+ let councilCompose = { workspaceContext: formatHistoryForCouncil(4) || "" };
37799
+ try {
37800
+ const { loadDurableContext: loadDurableContext2 } = await Promise.resolve().then(() => (init_loadDurableContext(), loadDurableContext_exports));
37801
+ const durableState = await loadDurableContext2(process.cwd());
37802
+ const composed = composeProjectContext2({
37803
+ mode: overrides.ragContext ? "zelari" : "council",
37804
+ cwd: process.cwd(),
37805
+ userMessage: effectiveText,
37806
+ memoryHits: overrides.ragContext,
37807
+ durableState: durableState || void 0,
37808
+ historySnippet: formatHistoryForCouncil(4) || void 0,
37809
+ includeLessons: true,
37810
+ includeDurableState: false
37811
+ });
37812
+ for (const w of composed.warnings) {
37813
+ appendSystem(setMessages, w, Date.now());
37814
+ }
37815
+ councilCompose = {
37816
+ workspaceContext: composed.workspaceContext,
37817
+ ...composed.ragContext ? { ragContext: composed.ragContext } : {}
37818
+ };
37819
+ } catch {
37820
+ if (overrides.ragContext) {
37821
+ councilCompose = {
37822
+ workspaceContext: formatHistoryForCouncil(4) || "",
37823
+ ragContext: overrides.ragContext
37824
+ };
37825
+ }
37826
+ }
37517
37827
  try {
37518
37828
  for await (const event of dispatchCouncil2(effectiveText, {
37519
37829
  apiKey: envConfig.apiKey,
@@ -37523,33 +37833,8 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
37523
37833
  sessionId,
37524
37834
  tools: councilToolRegistry,
37525
37835
  feedbackStore: councilFeedbackStore,
37526
- // v1.16: unified compose — product truth + draft plan ops + design index
37527
- // (not full vault). Memory RAG only via overrides.ragContext.
37528
- // Fail-soft: never block a council run if compose throws in tests/odd cwd.
37529
- ...(() => {
37530
- try {
37531
- const composed = composeProjectContext2({
37532
- mode: overrides.ragContext ? "zelari" : "council",
37533
- cwd: process.cwd(),
37534
- userMessage: effectiveText,
37535
- memoryHits: overrides.ragContext,
37536
- historySnippet: formatHistoryForCouncil(4) || void 0,
37537
- includeLessons: true
37538
- });
37539
- for (const w of composed.warnings) {
37540
- appendSystem(setMessages, w, Date.now());
37541
- }
37542
- return {
37543
- workspaceContext: composed.workspaceContext,
37544
- ...composed.ragContext ? { ragContext: composed.ragContext } : {}
37545
- };
37546
- } catch {
37547
- return {
37548
- workspaceContext: formatHistoryForCouncil(4) || "",
37549
- ...overrides.ragContext ? { ragContext: overrides.ragContext } : {}
37550
- };
37551
- }
37552
- })(),
37836
+ workspaceContext: councilCompose.workspaceContext,
37837
+ ...councilCompose.ragContext ? { ragContext: councilCompose.ragContext } : {},
37553
37838
  maxToolCallsPerTurn: councilMaxToolCalls,
37554
37839
  maxToolLoopIterations: councilMaxToolLoop,
37555
37840
  ...councilMaxToolLoopHard > 0 ? { maxToolLoopHardCap: councilMaxToolLoopHard } : {},
@@ -38848,20 +39133,28 @@ async function handleStateStatus(ctx) {
38848
39133
  if (!head) {
38849
39134
  appendSystem(
38850
39135
  ctx.setMessages,
38851
- "[state] no durable commits yet. Run a verified Zelari slice or `/state commit [label]`."
39136
+ "[state] no durable commits yet. Run a verified Zelari slice or `/state commit [label]`.\n Kill switch: ZELARI_STATE=0 disables the store."
38852
39137
  );
38853
39138
  return;
38854
39139
  }
38855
- const recent = await store.list(5);
39140
+ const discoveries = await store.loadDiscoveries(head.id);
39141
+ const reusable = discoveries.filter((d) => d.reusable).length;
39142
+ const recent = await store.list(8);
38856
39143
  const lines = recent.map((c, i) => {
38857
- const ver = c.verification.ran ? c.verification.ok ? "ok" : "fail" : "n/a";
38858
- return ` ${i === 0 ? "\u2192" : " "} ${c.id} ${ago2(c.createdAt)} ${c.label} ver=${ver}` + (c.layer ? ` [${c.layer}]` : "");
39144
+ const ver2 = c.verification.ran ? c.verification.ok ? "ok" : "fail" : "n/a";
39145
+ return ` ${i === 0 ? "\u2192" : " "} ${c.id} ${ago2(c.createdAt)} ${c.label} ver=${ver2}` + (c.layer ? ` [${c.layer}]` : "") + (c.stablePromptHash ? ` hash=${c.stablePromptHash.slice(0, 8)}` : "");
38859
39146
  });
39147
+ const ver = head.verification;
38860
39148
  appendSystem(
38861
39149
  ctx.setMessages,
38862
- `[state] HEAD ${head.id} \xB7 ${head.discoveryCount} discoveries \xB7 mode ${head.mode}
38863
- ` + (head.workspaceCheckpointId ? `linked checkpoint: ${head.workspaceCheckpointId}
38864
- ` : "") + lines.join("\n")
39150
+ `[state] HEAD ${head.id} \xB7 mode ${head.mode}` + (head.layer ? ` \xB7 layer ${head.layer}` : "") + `
39151
+ discoveries: ${head.discoveryCount} total, ${reusable} reusable
39152
+ verification: ran=${ver.ran} ok=${ver.ok}` + (ver.reportPath ? ` report=${ver.reportPath}` : "") + (head.parentId ? `
39153
+ parent: ${head.parentId}` : "") + (head.workspaceCheckpointId ? `
39154
+ workspaceCheckpoint: ${head.workspaceCheckpointId}` : "") + (head.stablePromptHash ? `
39155
+ stablePromptHash: ${head.stablePromptHash}` : "") + `
39156
+ created: ${ago2(head.createdAt)}
39157
+ ` + lines.join("\n")
38865
39158
  );
38866
39159
  }
38867
39160
  async function handleStateCommit(ctx, label) {
@@ -38916,19 +39209,16 @@ async function handleStateRestore(ctx, id, opts) {
38916
39209
  appendSystem(ctx.setMessages, res.message);
38917
39210
  }
38918
39211
 
38919
- // src/cli/state/promptCacheStats.ts
38920
- function formatCacheStatsLine(stats) {
38921
- const pct = stats.promptTokens > 0 ? Math.round(stats.hitRate * 100) : 0;
38922
- return `cache hit ${pct}% \xB7 premium ${stats.premiumTokens} \xB7 cached ${stats.cachedTokens} \xB7 stable busts ${stats.stableBustCount} \xB7 turns ${stats.turns}`;
38923
- }
38924
-
38925
39212
  // src/cli/slashHandlers/cache.ts
38926
39213
  function handleCacheStats(ctx) {
39214
+ const ttl = resolvePromptCacheTtl();
38927
39215
  const s = ctx.sessionStats;
38928
39216
  if (!s || !(s.promptTokens || s.cachedTokens || s.totalTokens)) {
38929
39217
  appendSystem(
38930
39218
  ctx.setMessages,
38931
- "[cache] no session usage yet. Complete a model turn to see hit rate.\n Tip: stable prompt (identity+tools) should stay byte-stable; workspace/plan is volatile.\n Env: ZELARI_PROMPT_CACHE_TTL=1h|5m|auto (Anthropic path when available)."
39219
+ `[cache] no session usage yet. Complete a model turn to see hit rate.
39220
+ Tip: stable prompt (identity+tools) should stay byte-stable; workspace/plan is volatile.
39221
+ ZELARI_PROMPT_CACHE_TTL=${ttl} (OpenAI-compat: automatic prefix cache; TTL is a preference for future Anthropic markers).`
38932
39222
  );
38933
39223
  return;
38934
39224
  }
@@ -38938,15 +39228,18 @@ function handleCacheStats(ctx) {
38938
39228
  premiumTokens: s.premiumTokens ?? Math.max(0, (s.promptTokens ?? 0) - (s.cachedTokens ?? 0)),
38939
39229
  hitRate: s.cacheHitRate ?? 0,
38940
39230
  estimatedCostUsd: s.totalCostUsd ?? 0,
39231
+ lastStableHash: s.lastStableHash,
38941
39232
  stableBustCount: s.stableBustCount ?? 0,
38942
39233
  turns: 0
38943
39234
  };
38944
39235
  const pct = stats.promptTokens > 0 ? Math.round(stats.hitRate * 100) : 0;
39236
+ const hashShort = stats.lastStableHash ? stats.lastStableHash.slice(0, 8) : "\u2014";
38945
39237
  appendSystem(
38946
39238
  ctx.setMessages,
38947
39239
  `[cache] ${formatCacheStatsLine(stats)}
38948
- hit rate: ${pct}% \xB7 cost ~$${stats.estimatedCostUsd.toFixed(4)}
38949
- (OpenAI-compat: automatic prefix cache; keep system stable prefix free of plan/memory churn.)`
39240
+ hit rate: ${pct}% \xB7 premium ${stats.premiumTokens} \xB7 cached ${stats.cachedTokens}
39241
+ cost ~$${stats.estimatedCostUsd.toFixed(4)} \xB7 stable hash ${hashShort} \xB7 busts ${stats.stableBustCount}
39242
+ TTL pref: ${ttl} (OpenAI-compat path uses automatic prefix caching; keep stable prefix free of plan/memory churn.)`
38950
39243
  );
38951
39244
  }
38952
39245
 
@@ -39168,18 +39461,65 @@ import { spawn as spawn10 } from "node:child_process";
39168
39461
  async function installPlugin(spec, cwd, executor = spawn10) {
39169
39462
  const scopeFlag = spec.installScope === "global" ? "-g" : "-D";
39170
39463
  const args = ["install", scopeFlag, spec.npmPackage];
39171
- const primary = await runNpm2(executor, args, cwd, "shim");
39172
- if (primary.ok) return primary;
39173
- const npmCli = resolveBundledNpmCli();
39174
- if (npmCli && looksLikeBrokenShim(primary.exitCode, primary.output)) {
39175
- const fallback = await runNpm2(executor, args, cwd, "bundled", npmCli);
39176
- return {
39177
- ...fallback,
39178
- output: `[plugins] npm shim failed (${primary.error ?? "exit " + primary.exitCode}); retried via bundled npm (${npmCli}).
39464
+ let result = await runNpm2(executor, args, cwd, "shim");
39465
+ if (!result.ok) {
39466
+ const npmCli = resolveBundledNpmCli();
39467
+ if (npmCli && looksLikeBrokenShim(result.exitCode, result.output)) {
39468
+ const fallback = await runNpm2(executor, args, cwd, "bundled", npmCli);
39469
+ result = {
39470
+ ...fallback,
39471
+ output: `[plugins] npm shim failed (${result.error ?? "exit " + result.exitCode}); retried via bundled npm (${npmCli}).
39179
39472
  ${fallback.output}`
39473
+ };
39474
+ }
39475
+ }
39476
+ if (!result.ok) return result;
39477
+ if (spec.id === "playwright") {
39478
+ const browsers = await runPlaywrightInstallChromium(cwd, executor);
39479
+ result = {
39480
+ ...result,
39481
+ ok: browsers.ok ? true : result.ok,
39482
+ // Prefer package success even if browser download fails — surface both.
39483
+ output: (result.output ? result.output + "\n" : "") + (browsers.output ? `[playwright browsers]
39484
+ ${browsers.output}` : "") + (browsers.ok ? "" : `
39485
+ [playwright] Chromium install failed: ${browsers.error ?? "unknown"}. Run: npx playwright install chromium`),
39486
+ error: browsers.ok ? result.error : result.error ?? `playwright package installed but Chromium download failed: ${browsers.error ?? "unknown"}`
39180
39487
  };
39488
+ result = { ...result, ok: true };
39181
39489
  }
39182
- return primary;
39490
+ return result;
39491
+ }
39492
+ function runPlaywrightInstallChromium(cwd, executor) {
39493
+ return new Promise((resolve) => {
39494
+ let stdout = "";
39495
+ let stderr = "";
39496
+ const stdio = ["ignore", "pipe", "pipe"];
39497
+ const args = ["playwright", "install", "chromium"];
39498
+ const child = process.platform === "win32" ? executor(buildCmdLine("npx", args), { stdio, shell: true, cwd }) : executor("npx", args, { stdio, cwd });
39499
+ child.stdout?.on("data", (chunk) => {
39500
+ stdout += chunk.toString();
39501
+ });
39502
+ child.stderr?.on("data", (chunk) => {
39503
+ stderr += chunk.toString();
39504
+ });
39505
+ child.on("error", (err) => {
39506
+ resolve({
39507
+ ok: false,
39508
+ output: stdout + stderr,
39509
+ error: err.message,
39510
+ exitCode: null
39511
+ });
39512
+ });
39513
+ child.on("close", (code) => {
39514
+ const ok = code === 0;
39515
+ resolve({
39516
+ ok,
39517
+ output: stdout + stderr,
39518
+ exitCode: code,
39519
+ error: ok ? void 0 : `npx playwright install chromium exited ${code}`
39520
+ });
39521
+ });
39522
+ });
39183
39523
  }
39184
39524
  function runNpm2(executor, args, cwd, mode, npmCliPath) {
39185
39525
  return new Promise((resolve) => {
@@ -40702,7 +41042,8 @@ function App() {
40702
41042
  premiumTokens: 0,
40703
41043
  cacheHitRate: 0,
40704
41044
  promptTokens: 0,
40705
- stableBustCount: 0
41045
+ stableBustCount: 0,
41046
+ lastStableHash: void 0
40706
41047
  });
40707
41048
  const [clearEpoch, setClearEpoch] = useState8(0);
40708
41049
  const [mode, setMode] = useState8("agent");
@@ -41816,11 +42157,16 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
41816
42157
  ].join("\n")
41817
42158
  };
41818
42159
  const { composeProjectContext: composeProjectContext2 } = await Promise.resolve().then(() => (init_composeContext(), composeContext_exports));
42160
+ const { loadDurableContext: loadDurableContext2 } = await Promise.resolve().then(() => (init_loadDurableContext(), loadDurableContext_exports));
42161
+ const cwd = process.cwd();
42162
+ const durableState = await loadDurableContext2(cwd);
41819
42163
  const composed = composeProjectContext2({
41820
42164
  mode: "agent",
41821
- cwd: process.cwd(),
42165
+ cwd,
41822
42166
  userMessage: opts.task,
41823
- includeLessons: false
42167
+ includeLessons: false,
42168
+ durableState: durableState || void 0,
42169
+ includeDurableState: false
41824
42170
  });
41825
42171
  let sshBlock = "";
41826
42172
  try {
@@ -41829,6 +42175,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
41829
42175
  } catch {
41830
42176
  }
41831
42177
  const rolePrompt = [headlessRole.systemPrompt, sshBlock].filter(Boolean).join("\n\n");
42178
+ const agentWorkspace = [composed.workspaceContext, composed.ragContext].filter(Boolean).join("\n\n");
41832
42179
  systemPrompt = buildSystemPrompt(
41833
42180
  { ...headlessRole, systemPrompt: rolePrompt },
41834
42181
  {
@@ -41836,7 +42183,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
41836
42183
  toolNames,
41837
42184
  mode: "agent",
41838
42185
  projectInstructions: composed.projectInstructions || void 0,
41839
- workspaceContext: composed.workspaceContext || void 0,
42186
+ workspaceContext: agentWorkspace || void 0,
41840
42187
  // Plan lives in workspaceContext as draft ops — never as RAG.
41841
42188
  ragContext: void 0,
41842
42189
  aiConfig: {
@@ -42104,11 +42451,16 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
42104
42451
  let currentAssistantText = "";
42105
42452
  try {
42106
42453
  const { composeProjectContext: composeProjectContext2 } = await Promise.resolve().then(() => (init_composeContext(), composeContext_exports));
42454
+ const { loadDurableContext: loadDurableContext2 } = await Promise.resolve().then(() => (init_loadDurableContext(), loadDurableContext_exports));
42455
+ const cwd = process.cwd();
42456
+ const durableState = await loadDurableContext2(cwd);
42107
42457
  const composed = composeProjectContext2({
42108
42458
  mode: "council",
42109
- cwd: process.cwd(),
42459
+ cwd,
42110
42460
  userMessage: opts.task,
42111
- includeLessons: true
42461
+ includeLessons: true,
42462
+ durableState: durableState || void 0,
42463
+ includeDurableState: false
42112
42464
  });
42113
42465
  for await (const event of dispatchCouncil2(effectiveTask, {
42114
42466
  apiKey: "REDACTED",
@@ -42249,13 +42601,17 @@ ${ragContext}` : slicePrompt;
42249
42601
  let membersCompleted = 0;
42250
42602
  const scrub = createStreamScrubber();
42251
42603
  const { composeProjectContext: composeProjectContext2 } = await Promise.resolve().then(() => (init_composeContext(), composeContext_exports));
42604
+ const { loadDurableContext: loadDurableContext2 } = await Promise.resolve().then(() => (init_loadDurableContext(), loadDurableContext_exports));
42252
42605
  const memOnly = ragContext?.trim() ? ragContext : void 0;
42606
+ const durableState = await loadDurableContext2(projectRoot);
42253
42607
  const composed = composeProjectContext2({
42254
42608
  mode: "zelari",
42255
42609
  cwd: projectRoot,
42256
42610
  userMessage: slicePrompt,
42257
42611
  memoryHits: memOnly,
42258
- includeLessons: true
42612
+ durableState: durableState || void 0,
42613
+ includeLessons: true,
42614
+ includeDurableState: false
42259
42615
  });
42260
42616
  for await (const event of dispatchCouncil2(fullPrompt, {
42261
42617
  apiKey: "REDACTED",
@@ -42625,6 +42981,98 @@ async function runDiscoverModels(providerArg) {
42625
42981
  }
42626
42982
  }
42627
42983
 
42984
+ // src/cli/plugins/cliFlags.ts
42985
+ init_registry2();
42986
+ function getArg(argv, flag) {
42987
+ const i = argv.indexOf(flag);
42988
+ if (i < 0) return void 0;
42989
+ return argv[i + 1];
42990
+ }
42991
+ function resolveCwd(argv) {
42992
+ return getArg(argv, "--cwd") ?? process.cwd();
42993
+ }
42994
+ function wantsPluginsStatus(argv) {
42995
+ return argv.includes("--plugins-status");
42996
+ }
42997
+ function wantsPluginsInstall(argv) {
42998
+ return argv.includes("--plugins-install");
42999
+ }
43000
+ async function runPluginsStatus(argv) {
43001
+ const cwd = resolveCwd(argv);
43002
+ const plugins = [];
43003
+ for (const spec of PLUGINS) {
43004
+ let present = false;
43005
+ try {
43006
+ present = await spec.detect(cwd);
43007
+ } catch {
43008
+ present = false;
43009
+ }
43010
+ plugins.push({
43011
+ id: spec.id,
43012
+ label: spec.label,
43013
+ present,
43014
+ description: spec.description,
43015
+ postInstallHint: spec.postInstallHint ?? null,
43016
+ npmPackage: spec.npmPackage,
43017
+ installScope: spec.installScope
43018
+ });
43019
+ }
43020
+ process.stdout.write(JSON.stringify({ cwd, plugins }, null, 2) + "\n");
43021
+ return 0;
43022
+ }
43023
+ async function runPluginsInstall(argv) {
43024
+ const cwd = resolveCwd(argv);
43025
+ const id = getArg(argv, "--plugins-install");
43026
+ if (!id) {
43027
+ process.stderr.write(
43028
+ "[plugins] usage: zelari-code --plugins-install <id> [--cwd <path>]\n"
43029
+ );
43030
+ process.stdout.write(
43031
+ JSON.stringify({
43032
+ ok: false,
43033
+ id: "",
43034
+ message: "missing plugin id"
43035
+ }) + "\n"
43036
+ );
43037
+ return 1;
43038
+ }
43039
+ const spec = findPlugin(id);
43040
+ if (!spec) {
43041
+ const msg = `unknown plugin id: ${id}. Available: ${PLUGINS.map((p3) => p3.id).join(", ")}`;
43042
+ process.stderr.write(`[plugins] ${msg}
43043
+ `);
43044
+ process.stdout.write(
43045
+ JSON.stringify({ ok: false, id, message: msg }) + "\n"
43046
+ );
43047
+ return 1;
43048
+ }
43049
+ process.stderr.write(
43050
+ `[plugins] installing ${spec.label} into ${cwd}\u2026
43051
+ `
43052
+ );
43053
+ const result = await installPlugin(spec, cwd);
43054
+ const payload = {
43055
+ ok: result.ok,
43056
+ id: spec.id,
43057
+ message: result.ok ? `Installed ${spec.label}` : result.error ?? `Install failed for ${spec.label}`,
43058
+ output: result.output?.slice(-4e3),
43059
+ postInstallHint: spec.postInstallHint ?? null
43060
+ };
43061
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
43062
+ if (!result.ok) {
43063
+ process.stderr.write(`[plugins] \u2717 ${payload.message}
43064
+ `);
43065
+ return 1;
43066
+ }
43067
+ process.stderr.write(`[plugins] \u2713 ${payload.message}
43068
+ `);
43069
+ if (spec.postInstallHint) {
43070
+ process.stderr.write(`[plugins] \u2192 ${spec.postInstallHint}
43071
+ `);
43072
+ }
43073
+ return 0;
43074
+ }
43075
+
42628
43076
  // src/cli/skillsMd.ts
42629
43077
  init_skills2();
42630
43078
  import { existsSync as existsSync36, readdirSync as readdirSync5, readFileSync as readFileSync30 } from "node:fs";
@@ -42809,6 +43257,14 @@ function pickRootComponent() {
42809
43257
  console.log(`zelari-code v${VERSION}`);
42810
43258
  process.exit(0);
42811
43259
  }
43260
+ if (wantsPluginsStatus(argv)) {
43261
+ void runPluginsStatus(argv).then((code) => process.exit(code));
43262
+ return { kind: "done" };
43263
+ }
43264
+ if (wantsPluginsInstall(argv)) {
43265
+ void runPluginsInstall(argv).then((code) => process.exit(code));
43266
+ return { kind: "done" };
43267
+ }
42812
43268
  if (argv.includes("--doctor") || argv.includes("doctor")) {
42813
43269
  const { runDoctor: runDoctor2 } = (init_doctor(), __toCommonJS(doctor_exports));
42814
43270
  void runDoctor2().then((healthy) => process.exit(healthy ? 0 : 1));
@@ -42838,7 +43294,7 @@ function pickRootComponent() {
42838
43294
  }
42839
43295
  if (argv.includes("--help") || argv.includes("-h")) {
42840
43296
  console.log(
42841
- "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode agent|council|zelari Dispatch mode (default: agent)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n --print-config Print provider/model config as JSON (no secrets)\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable (required)\n --args <json> JSON array of args (optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
43297
+ "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode agent|council|zelari Dispatch mode (default: agent)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n --print-config Print provider/model config as JSON (no secrets)\n --plugins-status JSON status of optional plugins (Playwright, eslint, \u2026)\n --plugins-install <id> Install plugin (playwright also fetches Chromium)\n --cwd <path> Workspace for -D installs (default: process.cwd())\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable (required)\n --args <json> JSON array of args (optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
42842
43298
  );
42843
43299
  process.exit(0);
42844
43300
  }