zelari-code 2.21.0 → 2.23.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.
@@ -3288,10 +3288,10 @@ function mergeDefs(...defs) {
3288
3288
  function cloneDef(schema) {
3289
3289
  return mergeDefs(schema._zod.def);
3290
3290
  }
3291
- function getElementAtPath(obj, path91) {
3292
- if (!path91)
3291
+ function getElementAtPath(obj, path92) {
3292
+ if (!path92)
3293
3293
  return obj;
3294
- return path91.reduce((acc, key) => acc?.[key], obj);
3294
+ return path92.reduce((acc, key) => acc?.[key], obj);
3295
3295
  }
3296
3296
  function promiseAllObject(promisesObj) {
3297
3297
  const keys = Object.keys(promisesObj);
@@ -3619,11 +3619,11 @@ function explicitlyAborted(x, startIndex = 0) {
3619
3619
  }
3620
3620
  return false;
3621
3621
  }
3622
- function prefixIssues(path91, issues) {
3622
+ function prefixIssues(path92, issues) {
3623
3623
  return issues.map((iss) => {
3624
3624
  var _a3;
3625
3625
  (_a3 = iss).path ?? (_a3.path = []);
3626
- iss.path.unshift(path91);
3626
+ iss.path.unshift(path92);
3627
3627
  return iss;
3628
3628
  });
3629
3629
  }
@@ -3841,16 +3841,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
3841
3841
  }
3842
3842
  function formatError(error51, mapper = (issue2) => issue2.message) {
3843
3843
  const fieldErrors = { _errors: [] };
3844
- const processError = (error52, path91 = []) => {
3844
+ const processError = (error52, path92 = []) => {
3845
3845
  for (const issue2 of error52.issues) {
3846
3846
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3847
- issue2.errors.map((issues) => processError({ issues }, [...path91, ...issue2.path]));
3847
+ issue2.errors.map((issues) => processError({ issues }, [...path92, ...issue2.path]));
3848
3848
  } else if (issue2.code === "invalid_key") {
3849
- processError({ issues: issue2.issues }, [...path91, ...issue2.path]);
3849
+ processError({ issues: issue2.issues }, [...path92, ...issue2.path]);
3850
3850
  } else if (issue2.code === "invalid_element") {
3851
- processError({ issues: issue2.issues }, [...path91, ...issue2.path]);
3851
+ processError({ issues: issue2.issues }, [...path92, ...issue2.path]);
3852
3852
  } else {
3853
- const fullpath = [...path91, ...issue2.path];
3853
+ const fullpath = [...path92, ...issue2.path];
3854
3854
  if (fullpath.length === 0) {
3855
3855
  fieldErrors._errors.push(mapper(issue2));
3856
3856
  } else {
@@ -3877,17 +3877,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
3877
3877
  }
3878
3878
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
3879
3879
  const result = { errors: [] };
3880
- const processError = (error52, path91 = []) => {
3880
+ const processError = (error52, path92 = []) => {
3881
3881
  var _a3, _b;
3882
3882
  for (const issue2 of error52.issues) {
3883
3883
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3884
- issue2.errors.map((issues) => processError({ issues }, [...path91, ...issue2.path]));
3884
+ issue2.errors.map((issues) => processError({ issues }, [...path92, ...issue2.path]));
3885
3885
  } else if (issue2.code === "invalid_key") {
3886
- processError({ issues: issue2.issues }, [...path91, ...issue2.path]);
3886
+ processError({ issues: issue2.issues }, [...path92, ...issue2.path]);
3887
3887
  } else if (issue2.code === "invalid_element") {
3888
- processError({ issues: issue2.issues }, [...path91, ...issue2.path]);
3888
+ processError({ issues: issue2.issues }, [...path92, ...issue2.path]);
3889
3889
  } else {
3890
- const fullpath = [...path91, ...issue2.path];
3890
+ const fullpath = [...path92, ...issue2.path];
3891
3891
  if (fullpath.length === 0) {
3892
3892
  result.errors.push(mapper(issue2));
3893
3893
  continue;
@@ -3919,8 +3919,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
3919
3919
  }
3920
3920
  function toDotPath(_path) {
3921
3921
  const segs = [];
3922
- const path91 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3923
- for (const seg of path91) {
3922
+ const path92 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3923
+ for (const seg of path92) {
3924
3924
  if (typeof seg === "number")
3925
3925
  segs.push(`[${seg}]`);
3926
3926
  else if (typeof seg === "symbol")
@@ -17423,13 +17423,13 @@ function resolveRef(ref, ctx) {
17423
17423
  if (!ref.startsWith("#")) {
17424
17424
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
17425
17425
  }
17426
- const path91 = ref.slice(1).split("/").filter(Boolean);
17427
- if (path91.length === 0) {
17426
+ const path92 = ref.slice(1).split("/").filter(Boolean);
17427
+ if (path92.length === 0) {
17428
17428
  return ctx.rootSchema;
17429
17429
  }
17430
17430
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
17431
- if (path91[0] === defsKey) {
17432
- const key = path91[1];
17431
+ if (path92[0] === defsKey) {
17432
+ const key = path92[1];
17433
17433
  if (!key || !ctx.defs[key]) {
17434
17434
  throw new Error(`Reference not found: ${ref}`);
17435
17435
  }
@@ -19887,11 +19887,11 @@ var init_tools = __esm({
19887
19887
  if (!ctx.addDocument)
19888
19888
  return "Knowledge vault tool not available.";
19889
19889
  const title = args["title"] || "New Document";
19890
- const path91 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
19890
+ const path92 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
19891
19891
  const content = args["content"] || "";
19892
19892
  const tags = args["tags"] || [];
19893
19893
  ctx.addDocument({
19894
- path: path91,
19894
+ path: path92,
19895
19895
  title,
19896
19896
  content,
19897
19897
  format: "markdown",
@@ -19900,7 +19900,7 @@ var init_tools = __esm({
19900
19900
  workspaceId: ctx.workspaceId
19901
19901
  });
19902
19902
  ctx.addActivity("vault", "created document", title);
19903
- return `Document "${title}" created at "${path91}".`;
19903
+ return `Document "${title}" created at "${path92}".`;
19904
19904
  }
19905
19905
  }
19906
19906
  ];
@@ -26246,11 +26246,11 @@ var init_synthesisAudit = __esm({
26246
26246
  import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
26247
26247
  import { join as join4 } from "node:path";
26248
26248
  function loadNfrSpec(zelariRoot) {
26249
- const path91 = join4(zelariRoot, "nfr-spec.json");
26250
- if (!existsSync7(path91))
26249
+ const path92 = join4(zelariRoot, "nfr-spec.json");
26250
+ if (!existsSync7(path92))
26251
26251
  return null;
26252
26252
  try {
26253
- const raw = JSON.parse(readFileSync7(path91, "utf8"));
26253
+ const raw = JSON.parse(readFileSync7(path92, "utf8"));
26254
26254
  if (raw.version !== 1 || !Array.isArray(raw.targets))
26255
26255
  return null;
26256
26256
  return raw;
@@ -28556,9 +28556,9 @@ var init_types5 = __esm({
28556
28556
  import { readFileSync as readFileSync12 } from "node:fs";
28557
28557
  import { join as join10 } from "node:path";
28558
28558
  function readLessonsDeduped(zelariRoot) {
28559
- const path91 = join10(zelariRoot, LESSONS_FILE);
28559
+ const path92 = join10(zelariRoot, LESSONS_FILE);
28560
28560
  try {
28561
- const raw = readFileSync12(path91, "utf8");
28561
+ const raw = readFileSync12(path92, "utf8");
28562
28562
  const byId = /* @__PURE__ */ new Map();
28563
28563
  for (const line of raw.split(/\r?\n/)) {
28564
28564
  if (!line.trim())
@@ -28659,8 +28659,8 @@ function keywordsFrom(check2, signature) {
28659
28659
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
28660
28660
  }
28661
28661
  function writeLesson(zelariRoot, lesson) {
28662
- const path91 = join11(zelariRoot, LESSONS_FILE);
28663
- appendFileSync(path91, `${JSON.stringify(lesson)}
28662
+ const path92 = join11(zelariRoot, LESSONS_FILE);
28663
+ appendFileSync(path92, `${JSON.stringify(lesson)}
28664
28664
  `, "utf8");
28665
28665
  }
28666
28666
  function findSimilar(lessons, signature) {
@@ -30624,9 +30624,9 @@ function findCycle(nodes) {
30624
30624
  if (color.get(start) !== WHITE)
30625
30625
  continue;
30626
30626
  const stack = [[start, 0]];
30627
- const path91 = [];
30627
+ const path92 = [];
30628
30628
  color.set(start, GRAY);
30629
- path91.push(start);
30629
+ path92.push(start);
30630
30630
  while (stack.length > 0) {
30631
30631
  const top = stack[stack.length - 1];
30632
30632
  const [id3, idx] = top;
@@ -30639,17 +30639,17 @@ function findCycle(nodes) {
30639
30639
  continue;
30640
30640
  const c = color.get(dep);
30641
30641
  if (c === GRAY) {
30642
- const at = path91.indexOf(dep);
30643
- return [...path91.slice(at), dep];
30642
+ const at = path92.indexOf(dep);
30643
+ return [...path92.slice(at), dep];
30644
30644
  }
30645
30645
  if (c === WHITE) {
30646
30646
  color.set(dep, GRAY);
30647
- path91.push(dep);
30647
+ path92.push(dep);
30648
30648
  stack.push([dep, 0]);
30649
30649
  }
30650
30650
  } else {
30651
30651
  color.set(id3, BLACK);
30652
- path91.pop();
30652
+ path92.pop();
30653
30653
  stack.pop();
30654
30654
  }
30655
30655
  }
@@ -31569,8 +31569,8 @@ var init_runner = __esm({
31569
31569
  failed: [...this.tentaclesById.values()].filter((r) => r.status === "error"),
31570
31570
  pending: []
31571
31571
  };
31572
- const path91 = await this.host.saveSnapshot(snapshot, ".zelari/kraken/snapshots");
31573
- this.host.log(`checkpoint: ${label ?? "auto"} \u2192 ${path91}`);
31572
+ const path92 = await this.host.saveSnapshot(snapshot, ".zelari/kraken/snapshots");
31573
+ this.host.log(`checkpoint: ${label ?? "auto"} \u2192 ${path92}`);
31574
31574
  return snapshot;
31575
31575
  }
31576
31576
  callLog(msg, data) {
@@ -32206,7 +32206,40 @@ var init_agentAdapter = __esm({
32206
32206
  import { promises as fs9 } from "node:fs";
32207
32207
  import path14 from "node:path";
32208
32208
  import crypto2 from "node:crypto";
32209
- var SessionLogLockedError, DEFAULT_STALE_LOCK_MS, SessionLogWriter;
32209
+ function defaultLockLivenessProbe(pid) {
32210
+ try {
32211
+ process.kill(pid, 0);
32212
+ return true;
32213
+ } catch (err) {
32214
+ return err.code !== "ESRCH";
32215
+ }
32216
+ }
32217
+ function resolveHeartbeatStaleMs(env = process.env) {
32218
+ const raw = env.ZELARI_SPINE_HEARTBEAT_STALE_MS;
32219
+ if (raw) {
32220
+ const parsed = Number.parseInt(raw, 10);
32221
+ if (Number.isFinite(parsed) && parsed > 0)
32222
+ return parsed;
32223
+ }
32224
+ return DEFAULT_HEARTBEAT_STALE_MS;
32225
+ }
32226
+ function evaluateLockTakeover(lockInfo, opts) {
32227
+ const heartbeatStaleMs = opts.heartbeatStaleMs ?? DEFAULT_HEARTBEAT_STALE_MS;
32228
+ const staleLockMs = opts.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
32229
+ const ts = typeof lockInfo.ts === "number" && Number.isFinite(lockInfo.ts) && lockInfo.ts > 0 ? lockInfo.ts : 0;
32230
+ const pid = typeof lockInfo.pid === "number" && Number.isFinite(lockInfo.pid) && lockInfo.pid > 0 ? lockInfo.pid : void 0;
32231
+ if (pid !== void 0) {
32232
+ if (!opts.probe(pid))
32233
+ return { takeover: true, reason: "pid-dead" };
32234
+ if (ts > 0 && opts.now - ts < heartbeatStaleMs)
32235
+ return { takeover: false, reason: "alive-fresh" };
32236
+ return { takeover: true, reason: "heartbeat-stale" };
32237
+ }
32238
+ if (ts > 0 && opts.now - ts > staleLockMs)
32239
+ return { takeover: true, reason: "stale" };
32240
+ return { takeover: false, reason: "no-pid-locked" };
32241
+ }
32242
+ var SessionLogLockedError, DEFAULT_STALE_LOCK_MS, DEFAULT_HEARTBEAT_STALE_MS, SessionLogWriter;
32210
32243
  var init_writer = __esm({
32211
32244
  "packages/core/dist/session/writer.js"() {
32212
32245
  "use strict";
@@ -32223,13 +32256,18 @@ var init_writer = __esm({
32223
32256
  }
32224
32257
  };
32225
32258
  DEFAULT_STALE_LOCK_MS = 10 * 60 * 1e3;
32259
+ DEFAULT_HEARTBEAT_STALE_MS = 12e4;
32226
32260
  SessionLogWriter = class _SessionLogWriter {
32227
32261
  sessionId;
32228
32262
  eventsPath;
32229
32263
  lockPath;
32230
32264
  ownership = crypto2.randomUUID();
32231
32265
  staleLockMs;
32266
+ heartbeatStaleMs;
32267
+ probe;
32232
32268
  now;
32269
+ /** Last heartbeat ts written into the lock (throttle window anchor). */
32270
+ lastHeartbeatAt = 0;
32233
32271
  nextSeq;
32234
32272
  closed = false;
32235
32273
  chain = Promise.resolve();
@@ -32239,6 +32277,8 @@ var init_writer = __esm({
32239
32277
  this.lockPath = path14.join(sessionDir, "writer.lock");
32240
32278
  this.nextSeq = startSeq;
32241
32279
  this.staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
32280
+ this.heartbeatStaleMs = options.heartbeatStaleMs ?? resolveHeartbeatStaleMs();
32281
+ this.probe = options.probe ?? defaultLockLivenessProbe;
32242
32282
  this.now = options.now ?? Date.now;
32243
32283
  }
32244
32284
  /** Create the writer and acquire the ownership lock. */
@@ -32261,6 +32301,7 @@ var init_writer = __esm({
32261
32301
  const payload = this.lockPayload();
32262
32302
  try {
32263
32303
  await fs9.writeFile(this.lockPath, payload, { encoding: "utf-8", flag: "wx", mode: 420 });
32304
+ this.lastHeartbeatAt = this.now();
32264
32305
  return;
32265
32306
  } catch (err) {
32266
32307
  if (err.code !== "EEXIST")
@@ -32268,19 +32309,43 @@ var init_writer = __esm({
32268
32309
  }
32269
32310
  const existing = await fs9.readFile(this.lockPath, "utf-8").catch(() => null);
32270
32311
  let owner = "unknown";
32271
- let ts = 0;
32312
+ const lockInfo = {};
32272
32313
  if (existing) {
32273
32314
  try {
32274
32315
  const parsed = JSON.parse(existing);
32275
32316
  owner = parsed.ownership ?? owner;
32276
- ts = parsed.ts ?? ts;
32317
+ lockInfo.ownership = parsed.ownership;
32318
+ lockInfo.pid = parsed.pid;
32319
+ lockInfo.ts = parsed.ts;
32277
32320
  } catch {
32278
32321
  }
32279
32322
  }
32280
- const stale = ts > 0 && this.now() - ts > this.staleLockMs;
32281
- if (!stale)
32323
+ const verdict = evaluateLockTakeover(lockInfo, {
32324
+ now: this.now(),
32325
+ probe: this.probe,
32326
+ heartbeatStaleMs: this.heartbeatStaleMs,
32327
+ staleLockMs: this.staleLockMs
32328
+ });
32329
+ if (!verdict.takeover)
32282
32330
  throw new SessionLogLockedError(this.eventsPath, owner);
32283
32331
  await fs9.writeFile(this.lockPath, payload, { encoding: "utf-8" });
32332
+ this.lastHeartbeatAt = this.now();
32333
+ }
32334
+ /** Min interval between lock-ts refreshes (≤ ~1 rewrite/second). */
32335
+ static HEARTBEAT_MIN_INTERVAL_MS = 1e3;
32336
+ /**
32337
+ * Lock heartbeat: while we own the lock, refresh its ts so concurrent
32338
+ * writers can tell a live owner from a hung one. Best-effort and throttled
32339
+ * to at most ~1 rewrite/second; it rides the append chain so close() always
32340
+ * lands AFTER the last heartbeat (never resurrects a removed lock).
32341
+ */
32342
+ maybeHeartbeat() {
32343
+ const ts = this.now();
32344
+ if (ts - this.lastHeartbeatAt < _SessionLogWriter.HEARTBEAT_MIN_INTERVAL_MS)
32345
+ return;
32346
+ this.lastHeartbeatAt = ts;
32347
+ void fs9.writeFile(this.lockPath, this.lockPayload(), { encoding: "utf-8" }).catch(() => {
32348
+ });
32284
32349
  }
32285
32350
  /** Append one event; returns the validated envelope with its assigned seq. */
32286
32351
  async append(input) {
@@ -32297,6 +32362,7 @@ var init_writer = __esm({
32297
32362
  };
32298
32363
  SessionEventEnvelopeSchema.parse(envelope);
32299
32364
  this.nextSeq += 1;
32365
+ this.chain = this.chain.then(() => this.maybeHeartbeat());
32300
32366
  const line = JSON.stringify(envelope) + "\n";
32301
32367
  this.chain = this.chain.then(() => fs9.appendFile(this.eventsPath, line, "utf-8"));
32302
32368
  await this.chain;
@@ -33771,7 +33837,7 @@ async function createExecutionContext(options = {}) {
33771
33837
  workspaceKind: workspace.kind
33772
33838
  }
33773
33839
  });
33774
- const fs45 = options.fs ?? new NodeFsProvider(workspace);
33840
+ const fs46 = options.fs ?? new NodeFsProvider(workspace);
33775
33841
  const shell = options.shell ?? new NodeShellProvider(workspace);
33776
33842
  const subagent = options.subagent ?? NOOP_SUBAGENT_PROVIDER;
33777
33843
  const env = options.env ?? process.env;
@@ -33779,7 +33845,7 @@ async function createExecutionContext(options = {}) {
33779
33845
  sessionId: sessionId2,
33780
33846
  profile,
33781
33847
  workspace,
33782
- fs: fs45,
33848
+ fs: fs46,
33783
33849
  shell,
33784
33850
  subagent,
33785
33851
  appendSessionEvent: (input) => writer.append(input),
@@ -34030,16 +34096,16 @@ function runRetentionFromEnv() {
34030
34096
  maxTotalBytes: Number.isFinite(parseMb) && parseMb > 0 ? Math.round(parseMb * 1024 * 1024) : DEFAULT_RUN_RETENTION_MAX_MB * 1024 * 1024
34031
34097
  };
34032
34098
  }
34033
- async function dirSize(path91) {
34099
+ async function dirSize(path92) {
34034
34100
  let total = 0;
34035
34101
  let entries;
34036
34102
  try {
34037
- entries = await readdir(path91, { withFileTypes: true });
34103
+ entries = await readdir(path92, { withFileTypes: true });
34038
34104
  } catch {
34039
34105
  return 0;
34040
34106
  }
34041
34107
  for (const entry of entries) {
34042
- const child = join13(path91, entry.name);
34108
+ const child = join13(path92, entry.name);
34043
34109
  if (entry.isDirectory())
34044
34110
  total += await dirSize(child);
34045
34111
  else {
@@ -34066,19 +34132,19 @@ async function enforceRunRetention(runsDir, options = {}) {
34066
34132
  for (const entry of entries) {
34067
34133
  if (!entry.isDirectory())
34068
34134
  continue;
34069
- const path91 = join13(runsDir, entry.name);
34135
+ const path92 = join13(runsDir, entry.name);
34070
34136
  let startedAt = 0;
34071
34137
  let endedAt;
34072
34138
  let completed = false;
34073
34139
  try {
34074
- const manifest = JSON.parse(await readFile(join13(path91, "manifest.json"), "utf8"));
34140
+ const manifest = JSON.parse(await readFile(join13(path92, "manifest.json"), "utf8"));
34075
34141
  startedAt = manifest.startedAt ?? 0;
34076
34142
  endedAt = manifest.endedAt;
34077
34143
  completed = Boolean(endedAt) && manifest.status !== "running";
34078
34144
  } catch {
34079
34145
  completed = false;
34080
34146
  }
34081
- infos.push({ name: entry.name, path: path91, startedAt, endedAt, completed, bytes: await dirSize(path91) });
34147
+ infos.push({ name: entry.name, path: path92, startedAt, endedAt, completed, bytes: await dirSize(path92) });
34082
34148
  }
34083
34149
  const remove = async (info) => {
34084
34150
  await rm(info.path, { recursive: true, force: true });
@@ -34850,12 +34916,12 @@ var init_engine = __esm({
34850
34916
  * content digest) and the returned ref carries the event seq when the
34851
34917
  * emitter resolved one.
34852
34918
  */
34853
- async fsEvidence(observation, path91, sha256, content, extra = {}) {
34919
+ async fsEvidence(observation, path92, sha256, content, extra = {}) {
34854
34920
  const digest = sha256 && content !== void 0 ? sha256(content) : void 0;
34855
- const seq = await this.emitEvidence({ observation, path: path91, ...extra, ...digest ? { digest } : {} });
34921
+ const seq = await this.emitEvidence({ observation, path: path92, ...extra, ...digest ? { digest } : {} });
34856
34922
  return {
34857
34923
  tier: "fs-observation",
34858
- ref: path91,
34924
+ ref: path92,
34859
34925
  capturedAt: Date.now(),
34860
34926
  ...digest ? { digest } : {},
34861
34927
  ...seq !== void 0 ? { seq } : {}
@@ -34863,25 +34929,25 @@ var init_engine = __esm({
34863
34929
  }
34864
34930
  async evalFileExists(check2, done) {
34865
34931
  const sha256 = this.options.sha256 ?? defaultSha256;
34866
- const fs45 = this.services.fs;
34867
- if (!fs45)
34932
+ const fs46 = this.services.fs;
34933
+ if (!fs46)
34868
34934
  return done({ status: "unknown", evidence: [], detail: "fs provider unavailable" });
34869
- const exists = await fs45.exists(check2.path);
34935
+ const exists = await fs46.exists(check2.path);
34870
34936
  if (!exists) {
34871
34937
  await this.emitEvidence({ observation: "file-exists", path: check2.path, exists: false });
34872
34938
  return done({ status: "fail", evidence: [], detail: `file not found: ${check2.path}` });
34873
34939
  }
34874
- const content = await fs45.readFile(check2.path).catch(() => "");
34940
+ const content = await fs46.readFile(check2.path).catch(() => "");
34875
34941
  return done({
34876
34942
  status: "pass",
34877
34943
  evidence: [await this.fsEvidence("file-exists", check2.path, sha256, content, { exists: true })]
34878
34944
  });
34879
34945
  }
34880
34946
  async evalFileAbsent(check2, done) {
34881
- const fs45 = this.services.fs;
34882
- if (!fs45)
34947
+ const fs46 = this.services.fs;
34948
+ if (!fs46)
34883
34949
  return done({ status: "unknown", evidence: [], detail: "fs provider unavailable" });
34884
- const exists = await fs45.exists(check2.path);
34950
+ const exists = await fs46.exists(check2.path);
34885
34951
  if (exists) {
34886
34952
  await this.emitEvidence({ observation: "file-absent", path: check2.path, exists: true });
34887
34953
  return done({ status: "fail", evidence: [], detail: `file still present: ${check2.path}` });
@@ -34893,12 +34959,12 @@ var init_engine = __esm({
34893
34959
  }
34894
34960
  async evalFileContains(check2, done) {
34895
34961
  const sha256 = this.options.sha256 ?? defaultSha256;
34896
- const fs45 = this.services.fs;
34897
- if (!fs45)
34962
+ const fs46 = this.services.fs;
34963
+ if (!fs46)
34898
34964
  return done({ status: "unknown", evidence: [], detail: "fs provider unavailable" });
34899
34965
  let content;
34900
34966
  try {
34901
- content = await fs45.readFile(check2.path);
34967
+ content = await fs46.readFile(check2.path);
34902
34968
  } catch {
34903
34969
  await this.emitEvidence({ observation: "file-contains", path: check2.path, readable: false });
34904
34970
  return done({ status: "fail", evidence: [], detail: `file not readable: ${check2.path}` });
@@ -35762,6 +35828,7 @@ __export(dist_exports, {
35762
35828
  CriterionSchema: () => CriterionSchema,
35763
35829
  CriterionSourceSchema: () => CriterionSourceSchema,
35764
35830
  DEFAULT_CONTEXT_POLICY: () => DEFAULT_CONTEXT_POLICY,
35831
+ DEFAULT_HEARTBEAT_STALE_MS: () => DEFAULT_HEARTBEAT_STALE_MS,
35765
35832
  DEFAULT_MAX_NODES: () => DEFAULT_MAX_NODES,
35766
35833
  DEFAULT_MAX_TENTACLES: () => DEFAULT_MAX_TENTACLES,
35767
35834
  DEFAULT_MEMORY_SCORING_WEIGHTS: () => DEFAULT_MEMORY_SCORING_WEIGHTS,
@@ -35992,6 +36059,7 @@ __export(dist_exports, {
35992
36059
  createGraph: () => createGraph,
35993
36060
  createRecorderObserver: () => createRecorderObserver,
35994
36061
  createRoutedRequestSnapshot: () => createRoutedRequestSnapshot,
36062
+ defaultLockLivenessProbe: () => defaultLockLivenessProbe,
35995
36063
  defaultPersonaParse: () => defaultPersonaParse,
35996
36064
  defaultResourcePolicy: () => defaultResourcePolicy,
35997
36065
  deriveInitialContract: () => deriveInitialContract,
@@ -36009,6 +36077,7 @@ __export(dist_exports, {
36009
36077
  enforceRunRetention: () => enforceRunRetention,
36010
36078
  evaluateBudgetContinuation: () => evaluateBudgetContinuation,
36011
36079
  evaluateCompletion: () => evaluateCompletion,
36080
+ evaluateLockTakeover: () => evaluateLockTakeover,
36012
36081
  evaluateMissionContinuation: () => evaluateMissionContinuation,
36013
36082
  evaluateResourceReserveGate: () => evaluateResourceReserveGate,
36014
36083
  executeTool: () => executeTool,
@@ -36160,6 +36229,7 @@ __export(dist_exports, {
36160
36229
  replayChairmanTextTools: () => replayChairmanTextTools,
36161
36230
  resolveAgentSkills: () => resolveAgentSkills,
36162
36231
  resolveCouncilRunMode: () => resolveCouncilRunMode,
36232
+ resolveHeartbeatStaleMs: () => resolveHeartbeatStaleMs,
36163
36233
  resolveInterventions: () => resolveInterventions,
36164
36234
  resolveJailed: () => resolveJailed,
36165
36235
  resolveMaxTentacles: () => resolveMaxTentacles,
@@ -37360,6 +37430,11 @@ var init_sessionSpine = __esm({
37360
37430
  } catch (err) {
37361
37431
  if (err instanceof SessionLogLockedError) {
37362
37432
  mirror.status = "locked";
37433
+ mirror.warnOnce(
37434
+ err,
37435
+ `session lock held by another (possibly dead) owner: ${err.message} \u2014 the turn resumed WITHOUT derived spine context`,
37436
+ "locked"
37437
+ );
37363
37438
  } else {
37364
37439
  mirror.status = "degraded";
37365
37440
  mirror.warnOnce(err);
@@ -37662,13 +37737,13 @@ var init_sessionSpine = __esm({
37662
37737
  appendEvent(input) {
37663
37738
  return this.append(input);
37664
37739
  }
37665
- warnOnce(err) {
37740
+ warnOnce(err, message, label = "degraded") {
37666
37741
  if (this.warned || this.options.quiet) return;
37667
37742
  this.warned = true;
37668
- const msg = err instanceof Error ? err.message : String(err);
37743
+ const msg = message ?? (err instanceof Error ? err.message : String(err));
37669
37744
  try {
37670
37745
  process.stderr.write(
37671
- `[zelari] session spine degraded for ${this.sessionId.slice(0, 8)}\u2026: ${msg} (transcript continues on the 1.x log)
37746
+ `[zelari] session spine ${label} for ${this.sessionId.slice(0, 8)}\u2026: ${msg} (transcript continues on the 1.x log)
37672
37747
  `
37673
37748
  );
37674
37749
  } catch {
@@ -38029,9 +38104,9 @@ function spillToolOutput(fullText, meta3) {
38029
38104
  const rnd = randomBytes3(3).toString("hex");
38030
38105
  const safeTool = (meta3?.toolName ?? "tool").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
38031
38106
  const file2 = `${stamp}-${safeTool}-${hash3}-${rnd}.txt`;
38032
- const path91 = join14(dir, file2);
38033
- writeFileSync11(path91, fullText, "utf8");
38034
- return path91;
38107
+ const path92 = join14(dir, file2);
38108
+ writeFileSync11(path92, fullText, "utf8");
38109
+ return path92;
38035
38110
  } catch {
38036
38111
  return null;
38037
38112
  }
@@ -38077,10 +38152,10 @@ function truncateToolResult(text, capOrOpts = TOOL_RESULT_LINE_CAP) {
38077
38152
  ${tail2}`;
38078
38153
  }
38079
38154
  if (doSpill) {
38080
- const path91 = spillToolOutput(text, { toolName: opts.toolName });
38081
- if (path91) {
38155
+ const path92 = spillToolOutput(text, { toolName: opts.toolName });
38156
+ if (path92) {
38082
38157
  const spillNote = `
38083
- \u2026 [full output spilled to: ${path91} \u2014 re-read with read_file if you need the complete text] \u2026`;
38158
+ \u2026 [full output spilled to: ${path92} \u2014 re-read with read_file if you need the complete text] \u2026`;
38084
38159
  if (preview.includes("] \u2026\n")) {
38085
38160
  preview = preview.replace("] \u2026\n", `] \u2026${spillNote}
38086
38161
  `);
@@ -42052,28 +42127,28 @@ var init_storage = __esm({
42052
42127
  VALID_SCALARS = /^(true|false|null|~)$/i;
42053
42128
  Storage = class {
42054
42129
  /** Read a Markdown file with frontmatter. Throws if not found. */
42055
- read(path91) {
42056
- if (!existsSync19(path91)) {
42057
- throw new Error(`File not found: ${path91}`);
42130
+ read(path92) {
42131
+ if (!existsSync19(path92)) {
42132
+ throw new Error(`File not found: ${path92}`);
42058
42133
  }
42059
- const md = readFileSync16(path91, "utf8");
42134
+ const md = readFileSync16(path92, "utf8");
42060
42135
  return parseFrontmatter(md);
42061
42136
  }
42062
42137
  /** Read a Markdown file; returns null if not found. */
42063
- readIfExists(path91) {
42064
- if (!existsSync19(path91)) return null;
42065
- return this.read(path91);
42138
+ readIfExists(path92) {
42139
+ if (!existsSync19(path92)) return null;
42140
+ return this.read(path92);
42066
42141
  }
42067
42142
  /**
42068
42143
  * Write a Markdown file atomically (tmp + rename). Creates parent dirs.
42069
42144
  * The meta object is serialized as YAML frontmatter; body as Markdown.
42070
42145
  */
42071
- write(path91, meta3, body) {
42072
- mkdirSync10(dirname2(path91), { recursive: true });
42073
- const tmp = path91 + ".tmp-" + process.pid;
42146
+ write(path92, meta3, body) {
42147
+ mkdirSync10(dirname2(path92), { recursive: true });
42148
+ const tmp = path92 + ".tmp-" + process.pid;
42074
42149
  const md = serializeFrontmatter(meta3, body);
42075
42150
  writeFileSync13(tmp, md, "utf8");
42076
- renameSync(tmp, path91);
42151
+ renameSync(tmp, path92);
42077
42152
  }
42078
42153
  /** List all .md files in a directory (non-recursive). */
42079
42154
  listMarkdown(dir) {
@@ -42135,8 +42210,8 @@ function nextPlanTaskId(store6) {
42135
42210
  return `t${store6.counter}`;
42136
42211
  }
42137
42212
  function writePlanTaskArtifact(rootDir, task) {
42138
- const path91 = join18(rootDir, "plan-tasks", `${task.id}.md`);
42139
- mkdirSync11(dirname3(path91), { recursive: true });
42213
+ const path92 = join18(rootDir, "plan-tasks", `${task.id}.md`);
42214
+ mkdirSync11(dirname3(path92), { recursive: true });
42140
42215
  const meta3 = {
42141
42216
  kind: "task",
42142
42217
  id: task.id,
@@ -42157,7 +42232,7 @@ function writePlanTaskArtifact(rootDir, task) {
42157
42232
  task.notes?.trim() ? task.notes.trim() : "_(no notes)_",
42158
42233
  ""
42159
42234
  ].filter((l) => l !== null).join("\n");
42160
- new Storage().write(path91, meta3, body);
42235
+ new Storage().write(path92, meta3, body);
42161
42236
  }
42162
42237
  function loadHandle(rootDir) {
42163
42238
  const jsonPath = join18(rootDir, "plan.json");
@@ -42863,6 +42938,100 @@ var init_inspectCommand = __esm({
42863
42938
  }
42864
42939
  });
42865
42940
 
42941
+ // src/cli/safety/selfKillGuard.ts
42942
+ function cleanToken(raw) {
42943
+ return raw.replace(/^["']+|["',]+\s*$/g, "").trim();
42944
+ }
42945
+ function isProtectedImage(name) {
42946
+ return PROTECTED_IMAGE_RE.test(cleanToken(name));
42947
+ }
42948
+ function stripSubstitutions(cmd) {
42949
+ return cmd.replace(/\$\([^)]*\)/g, " ( ) ").replace(/\$\{[^}]*\}/g, " ").replace(/`[^`]*`/g, " ` ` ");
42950
+ }
42951
+ function findImageKill(cmd) {
42952
+ const im = cmd.match(/\/{1,2}im\s+["']?([a-z0-9_.-]+)/i);
42953
+ if (im && /\btaskkill\b/i.test(cmd) && isProtectedImage(im[1])) {
42954
+ return `taskkill /IM ${cleanToken(im[1])}`;
42955
+ }
42956
+ const fi = cmd.match(/imagename\s+eq\s+["']?([a-z0-9_.-]+)/i);
42957
+ if (fi && /\btaskkill\b/i.test(cmd) && isProtectedImage(fi[1])) {
42958
+ return `taskkill /FI "IMAGENAME eq ${cleanToken(fi[1])}"`;
42959
+ }
42960
+ const nm = cmd.match(/\b(?:stop-process|kill)\b[^|;&]*-name\s+["']?([a-z0-9_.-]+)/i);
42961
+ if (nm && isProtectedImage(nm[1])) {
42962
+ return `Stop-Process -Name ${cleanToken(nm[1])}`;
42963
+ }
42964
+ if (/\bget-process\b[^|;&]*\bnode(?:\.exe)?\b/i.test(cmd) && /\|\s*[^|]*?(?:stop-process|\.\s*kill\s*\(|\bkill\b)/i.test(cmd)) {
42965
+ return "Get-Process node | Stop-Process";
42966
+ }
42967
+ if (/\bpkill\b[^|;&]*\bnode(?:\.exe)?\b/i.test(cmd)) return "pkill node";
42968
+ if (/\bkillall\b[^|;&]*\bnode(?:\.exe)?\b/i.test(cmd)) return "killall node";
42969
+ if (/\bwmic\b[^|;&]*name\s*=\s*["']?node(?:\.exe)?["']?/i.test(cmd) && /\b(?:delete|terminate)\b/i.test(cmd)) {
42970
+ return "wmic process delete node.exe";
42971
+ }
42972
+ return null;
42973
+ }
42974
+ function findPsPipelineKill(cmd) {
42975
+ if (!/\bps\b[^|;&]*\|\s*(?:grep|findstr|awk|sed)[^|;&]*\bnode(?:\.exe)?\b/i.test(cmd)) {
42976
+ return null;
42977
+ }
42978
+ if (!/\b(?:xargs\s+)?(?:kill|taskkill|pkill|killall|stop-process)\b/i.test(cmd)) {
42979
+ return null;
42980
+ }
42981
+ return "ps | grep node | kill";
42982
+ }
42983
+ function findProtectedPidKill(cmd, ctx) {
42984
+ const protectedPids = /* @__PURE__ */ new Set([
42985
+ ctx.selfPid,
42986
+ ...ctx.parentPid !== void 0 ? [ctx.parentPid] : [],
42987
+ ...ctx.extraProtectedPids ?? []
42988
+ ]);
42989
+ const hit = (pid, shape) => protectedPids.has(pid) ? `${shape} ${pid}` : null;
42990
+ const stripped = stripSubstitutions(cmd);
42991
+ for (const m of stripped.matchAll(/\/{1,2}pid(?:list)?\s+([\d,\s]+)/gi)) {
42992
+ for (const tok of m[1].split(/[\s,]+/)) {
42993
+ const pid = Number.parseInt(tok, 10);
42994
+ if (Number.isFinite(pid) && pid > 0) {
42995
+ const h = hit(pid, "taskkill /PID");
42996
+ if (h) return h;
42997
+ }
42998
+ }
42999
+ }
43000
+ for (const m of stripped.matchAll(/\b(?:stop-process|kill)\b[^|;&]*?-(?:id|pid)\s+(\d+)/gi)) {
43001
+ const h = hit(Number.parseInt(m[1], 10), "Stop-Process -Id");
43002
+ if (h) return h;
43003
+ }
43004
+ for (const m of stripped.matchAll(/\bkill\b([^|;&]*)/gi)) {
43005
+ for (const tok of m[1].split(/\s+/)) {
43006
+ if (!/^\d+$/.test(tok)) continue;
43007
+ const h = hit(Number.parseInt(tok, 10), "kill");
43008
+ if (h) return h;
43009
+ }
43010
+ }
43011
+ return null;
43012
+ }
43013
+ function blockedVerdict(reason, match) {
43014
+ return { blocked: true, reason, match, message: SELF_KILL_DENIAL_MESSAGE };
43015
+ }
43016
+ function inspectCommand(command, ctx) {
43017
+ if (typeof command !== "string" || command.length === 0) return { blocked: false };
43018
+ const imageMatch = findImageKill(command);
43019
+ if (imageMatch) return blockedVerdict("node-image", imageMatch);
43020
+ const pipelineMatch = findPsPipelineKill(command);
43021
+ if (pipelineMatch) return blockedVerdict("ps-pipeline", pipelineMatch);
43022
+ const pidMatch = findProtectedPidKill(command, ctx);
43023
+ if (pidMatch) return blockedVerdict("protected-pid", pidMatch);
43024
+ return { blocked: false };
43025
+ }
43026
+ var SELF_KILL_DENIAL_MESSAGE, PROTECTED_IMAGE_RE;
43027
+ var init_selfKillGuard = __esm({
43028
+ "src/cli/safety/selfKillGuard.ts"() {
43029
+ "use strict";
43030
+ SELF_KILL_DENIAL_MESSAGE = "Blocked: this command would kill the agent host process itself (node image / own process tree). To stop a dev server, kill by PORT instead: run `netstat -ano | findstr :<port>` to find the listener PID, then `taskkill //PID <listener-pid>` \u2014 never kill node by image name.";
43031
+ PROTECTED_IMAGE_RE = /^(?:node(?:\.exe)?|zelari(?:-code)?(?:\.exe)?)$/i;
43032
+ }
43033
+ });
43034
+
42866
43035
  // src/cli/safety/policyLoadMode.ts
42867
43036
  var policyLoadMode_exports = {};
42868
43037
  __export(policyLoadMode_exports, {
@@ -43252,6 +43421,13 @@ function createExecProcessTool(root, opts = {}) {
43252
43421
  if (blocked) {
43253
43422
  return typedErr(`[shell-blocked] exec_process refused (${blocked.reason}): ${blocked.pattern}`);
43254
43423
  }
43424
+ const selfKill = inspectCommand([input.program, ...argv].join(" "), {
43425
+ selfPid: process.pid,
43426
+ parentPid: process.ppid
43427
+ });
43428
+ if (selfKill.blocked && selfKill.message) {
43429
+ return typedErr(`[self-kill] exec_process refused: ${selfKill.message}`);
43430
+ }
43255
43431
  let cwd;
43256
43432
  try {
43257
43433
  cwd = resolveSandboxedPath(input.cwd ?? ".", { root });
@@ -43326,6 +43502,7 @@ var init_execProcess = __esm({
43326
43502
  init_zod();
43327
43503
  init_toolTypes();
43328
43504
  init_shellBlocklist();
43505
+ init_selfKillGuard();
43329
43506
  init_sandboxPath();
43330
43507
  init_osJail();
43331
43508
  execProcessInputSchema = external_exports.object({
@@ -44725,7 +44902,10 @@ var init_semantic2 = __esm({
44725
44902
  // src/cli/provider/openai-compatible.ts
44726
44903
  var openai_compatible_exports = {};
44727
44904
  __export(openai_compatible_exports, {
44905
+ PROVIDER_CONNECT_TIMEOUT_MS: () => PROVIDER_CONNECT_TIMEOUT_MS,
44728
44906
  PROVIDER_ENDPOINTS: () => PROVIDER_ENDPOINTS,
44907
+ PROVIDER_STREAM_IDLE_MS: () => PROVIDER_STREAM_IDLE_MS,
44908
+ PROVIDER_STREAM_MAX_MS: () => PROVIDER_STREAM_MAX_MS,
44729
44909
  dataUriFromImage: () => dataUriFromImage,
44730
44910
  modelSupportsVision: () => modelSupportsVision,
44731
44911
  openaiCompatibleProvider: () => openaiCompatibleProvider,
@@ -45828,21 +46008,21 @@ function normalizeAuth(auth) {
45828
46008
  return "agent";
45829
46009
  }
45830
46010
  function readSecrets() {
45831
- const path91 = getSshSecretsPath();
45832
- if (!existsSync25(path91)) return {};
46011
+ const path92 = getSshSecretsPath();
46012
+ if (!existsSync25(path92)) return {};
45833
46013
  try {
45834
- return JSON.parse(readFileSync20(path91, "utf8"));
46014
+ return JSON.parse(readFileSync20(path92, "utf8"));
45835
46015
  } catch {
45836
46016
  return {};
45837
46017
  }
45838
46018
  }
45839
46019
  function writeSecrets(data) {
45840
- const path91 = getSshSecretsPath();
45841
- mkdirSync12(dirname4(path91), { recursive: true });
45842
- writeFileSync15(path91, `${JSON.stringify(data, null, 2)}
46020
+ const path92 = getSshSecretsPath();
46021
+ mkdirSync12(dirname4(path92), { recursive: true });
46022
+ writeFileSync15(path92, `${JSON.stringify(data, null, 2)}
45843
46023
  `, "utf8");
45844
46024
  try {
45845
- chmodSync(path91, 384);
46025
+ chmodSync(path92, 384);
45846
46026
  } catch {
45847
46027
  }
45848
46028
  }
@@ -45871,10 +46051,10 @@ function deleteSshPassword(id3) {
45871
46051
  writeSecrets({ passwords });
45872
46052
  }
45873
46053
  function readStore2() {
45874
- const path91 = getSshTargetsPath();
45875
- if (!existsSync25(path91)) return [];
46054
+ const path92 = getSshTargetsPath();
46055
+ if (!existsSync25(path92)) return [];
45876
46056
  try {
45877
- const parsed = JSON.parse(readFileSync20(path91, "utf8"));
46057
+ const parsed = JSON.parse(readFileSync20(path92, "utf8"));
45878
46058
  const list = Array.isArray(parsed.targets) ? parsed.targets : [];
45879
46059
  return list.filter(
45880
46060
  (t) => t && typeof t.id === "string" && typeof t.host === "string" && typeof t.user === "string"
@@ -45889,11 +46069,11 @@ function readStore2() {
45889
46069
  }
45890
46070
  }
45891
46071
  function writeStore2(targets) {
45892
- const path91 = getSshTargetsPath();
45893
- mkdirSync12(dirname4(path91), { recursive: true });
46072
+ const path92 = getSshTargetsPath();
46073
+ mkdirSync12(dirname4(path92), { recursive: true });
45894
46074
  const clean = targets.map(({ hasPassword: _hp, ...t }) => t);
45895
46075
  writeFileSync15(
45896
- path91,
46076
+ path92,
45897
46077
  `${JSON.stringify({ targets: clean }, null, 2)}
45898
46078
  `,
45899
46079
  "utf8"
@@ -46139,11 +46319,11 @@ function formatSshTargetsForPrompt() {
46139
46319
  ];
46140
46320
  for (const t of targets) {
46141
46321
  const tags = t.tags?.length ? ` tags=[${t.tags.join(",")}]` : "";
46142
- const path91 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
46322
+ const path92 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
46143
46323
  const allow = t.allowedCommands?.length ? ` allowed=${t.allowedCommands.join("|")}` : " allowed=status-only";
46144
46324
  const auth = t.auth === "password" ? " auth=password" : t.auth === "keyPath" ? " auth=key" : " auth=agent";
46145
46325
  lines.push(
46146
- `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path91}${tags}${allow}`
46326
+ `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path92}${tags}${allow}`
46147
46327
  );
46148
46328
  }
46149
46329
  return lines.join("\n");
@@ -46760,14 +46940,27 @@ function anthropicMessagesProvider(config2) {
46760
46940
  const base2 = config2.baseUrl.replace(/\/$/, "").replace(/\/v1$/, "");
46761
46941
  const url2 = `${base2}/v1/messages`;
46762
46942
  let response;
46943
+ const connectController = new AbortController();
46944
+ const connectTimer = setTimeout(
46945
+ () => connectController.abort(
46946
+ new Error(
46947
+ `Provider connect timeout after ${Math.round(PROVIDER_CONNECT_TIMEOUT_MS / 1e3)}s (no response headers). Override ZELARI_PROVIDER_CONNECT_TIMEOUT_MS.`
46948
+ )
46949
+ ),
46950
+ PROVIDER_CONNECT_TIMEOUT_MS
46951
+ );
46952
+ const signals = [connectController.signal];
46953
+ if (params.signal) signals.push(params.signal);
46763
46954
  try {
46764
46955
  response = await fetch(url2, {
46765
46956
  method: "POST",
46766
46957
  headers: authHeaders(config2.apiKey, ttlPref === "1h"),
46767
46958
  body: JSON.stringify(body),
46768
- signal: params.signal
46959
+ signal: signals.length === 1 ? signals[0] : AbortSignal.any(signals)
46769
46960
  });
46961
+ clearTimeout(connectTimer);
46770
46962
  } catch (err) {
46963
+ clearTimeout(connectTimer);
46771
46964
  yield {
46772
46965
  kind: "error",
46773
46966
  message: `Network error: ${err instanceof Error ? err.message : String(err)}`
@@ -46802,9 +46995,17 @@ function anthropicMessagesProvider(config2) {
46802
46995
  };
46803
46996
  currentTool = null;
46804
46997
  };
46998
+ const streamStartedAt = Date.now();
46999
+ let lastUsefulAt = streamStartedAt;
47000
+ const streamDeadline = streamStartedAt + PROVIDER_STREAM_MAX_MS;
46805
47001
  try {
46806
47002
  while (true) {
46807
- const { value, done } = await reader.read();
47003
+ const { value, done } = await readChunkWithTimeout(reader, {
47004
+ idleMs: PROVIDER_STREAM_IDLE_MS,
47005
+ deadlineMs: streamDeadline,
47006
+ signal: params.signal,
47007
+ lastUsefulAt: () => lastUsefulAt
47008
+ });
46808
47009
  if (done) break;
46809
47010
  buffer += decoder.decode(value, { stream: true });
46810
47011
  const lines = buffer.split("\n");
@@ -46821,6 +47022,7 @@ function anthropicMessagesProvider(config2) {
46821
47022
  continue;
46822
47023
  }
46823
47024
  const type = ev.type;
47025
+ if (type !== "ping") lastUsefulAt = Date.now();
46824
47026
  if (type === "content_block_delta") {
46825
47027
  const delta = ev.delta;
46826
47028
  if (delta?.type === "text_delta" && typeof delta.text === "string") {
@@ -46894,6 +47096,7 @@ var ANTHROPIC_VERSION, ANTHROPIC_BETA, ANTHROPIC_BETA_EXTENDED_CACHE_TTL;
46894
47096
  var init_anthropic = __esm({
46895
47097
  "src/cli/provider/anthropic.ts"() {
46896
47098
  "use strict";
47099
+ init_openai_compatible();
46897
47100
  init_chatStats();
46898
47101
  init_thinking();
46899
47102
  ANTHROPIC_VERSION = "2023-06-01";
@@ -46972,14 +47175,27 @@ function chatgptResponsesProvider(config2) {
46972
47175
  const base2 = config2.baseUrl.replace(/\/$/, "");
46973
47176
  const url2 = `${base2}/responses`;
46974
47177
  let response;
47178
+ const connectController = new AbortController();
47179
+ const connectTimer = setTimeout(
47180
+ () => connectController.abort(
47181
+ new Error(
47182
+ `Provider connect timeout after ${Math.round(PROVIDER_CONNECT_TIMEOUT_MS / 1e3)}s (no response headers). Override ZELARI_PROVIDER_CONNECT_TIMEOUT_MS.`
47183
+ )
47184
+ ),
47185
+ PROVIDER_CONNECT_TIMEOUT_MS
47186
+ );
47187
+ const signals = [connectController.signal];
47188
+ if (params.signal) signals.push(params.signal);
46975
47189
  try {
46976
47190
  response = await fetch(url2, {
46977
47191
  method: "POST",
46978
47192
  headers: headers(config2),
46979
47193
  body: JSON.stringify(body),
46980
- signal: params.signal
47194
+ signal: signals.length === 1 ? signals[0] : AbortSignal.any(signals)
46981
47195
  });
47196
+ clearTimeout(connectTimer);
46982
47197
  } catch (err) {
47198
+ clearTimeout(connectTimer);
46983
47199
  yield {
46984
47200
  kind: "error",
46985
47201
  message: `Network error: ${err instanceof Error ? err.message : String(err)}`
@@ -47009,9 +47225,17 @@ function chatgptResponsesProvider(config2) {
47009
47225
  emittedTool = true;
47010
47226
  yield { kind: "tool_call", toolCallId: t.id, toolName: t.name, args };
47011
47227
  };
47228
+ const streamStartedAt = Date.now();
47229
+ let lastUsefulAt = streamStartedAt;
47230
+ const streamDeadline = streamStartedAt + PROVIDER_STREAM_MAX_MS;
47012
47231
  try {
47013
47232
  while (true) {
47014
- const { value, done } = await reader.read();
47233
+ const { value, done } = await readChunkWithTimeout(reader, {
47234
+ idleMs: PROVIDER_STREAM_IDLE_MS,
47235
+ deadlineMs: streamDeadline,
47236
+ signal: params.signal,
47237
+ lastUsefulAt: () => lastUsefulAt
47238
+ });
47015
47239
  if (done) break;
47016
47240
  buffer += decoder.decode(value, { stream: true });
47017
47241
  const lines = buffer.split("\n");
@@ -47028,6 +47252,7 @@ function chatgptResponsesProvider(config2) {
47028
47252
  continue;
47029
47253
  }
47030
47254
  const type = typeof ev.type === "string" ? ev.type : "";
47255
+ if (type) lastUsefulAt = Date.now();
47031
47256
  if (type === "response.output_text.delta" && typeof ev.delta === "string") {
47032
47257
  yield { kind: "text", delta: ev.delta };
47033
47258
  } else if (type === "response.reasoning_text.delta" && typeof ev.delta === "string") {
@@ -47083,6 +47308,7 @@ function chatgptResponsesProvider(config2) {
47083
47308
  var init_chatgpt = __esm({
47084
47309
  "src/cli/provider/chatgpt.ts"() {
47085
47310
  "use strict";
47311
+ init_openai_compatible();
47086
47312
  init_thinking();
47087
47313
  }
47088
47314
  });
@@ -49152,6 +49378,23 @@ function wrapWithShellSafety(original, audit, sessionId2, root, jail) {
49152
49378
  }
49153
49379
  throw err;
49154
49380
  }
49381
+ const selfKill = inspectCommand(cmd, { selfPid: process.pid, parentPid: process.ppid });
49382
+ if (selfKill.blocked && selfKill.message) {
49383
+ await audit.append({
49384
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
49385
+ sessionId: sessionId2,
49386
+ tool: original.name,
49387
+ args: redactForAudit(args),
49388
+ ok: false,
49389
+ resultSummary: selfKill.match ?? "self_kill",
49390
+ durationMs: 0,
49391
+ error: "self_kill_blocked"
49392
+ });
49393
+ return {
49394
+ ok: false,
49395
+ error: `[self-kill] ${selfKill.message}`
49396
+ };
49397
+ }
49155
49398
  }
49156
49399
  const rawCwd = args["cwd"];
49157
49400
  try {
@@ -49278,6 +49521,7 @@ var init_toolRegistry = __esm({
49278
49521
  init_resourceClaims();
49279
49522
  init_osJail();
49280
49523
  init_toolResultCache();
49524
+ init_selfKillGuard();
49281
49525
  init_toolTypes();
49282
49526
  init_skills2();
49283
49527
  HARNESS_BUILTIN_NAMES = /* @__PURE__ */ new Set([
@@ -54585,8 +54829,8 @@ function readPlan(ctx) {
54585
54829
  } catch {
54586
54830
  }
54587
54831
  }
54588
- const path91 = workspaceFile(ctx.rootDir, "plan");
54589
- const doc = ctx.storage.readIfExists(path91);
54832
+ const path92 = workspaceFile(ctx.rootDir, "plan");
54833
+ const doc = ctx.storage.readIfExists(path92);
54590
54834
  if (!doc) return { phases: [], tasks: [], milestones: [] };
54591
54835
  const meta3 = doc.meta;
54592
54836
  return {
@@ -54764,7 +55008,7 @@ function addMilestoneRecord(ctx, summary, input) {
54764
55008
  dueDate: input.dueDate,
54765
55009
  targetVersion: version2
54766
55010
  });
54767
- const path91 = join31(ctx.rootDir, "milestones", `${id3}.md`);
55011
+ const path92 = join31(ctx.rootDir, "milestones", `${id3}.md`);
54768
55012
  const meta3 = {
54769
55013
  kind: "milestone",
54770
55014
  id: id3,
@@ -54781,7 +55025,7 @@ function addMilestoneRecord(ctx, summary, input) {
54781
55025
  `Target version: ${version2}`,
54782
55026
  ""
54783
55027
  ].join("\n");
54784
- ctx.storage.write(path91, meta3, body);
55028
+ ctx.storage.write(path92, meta3, body);
54785
55029
  return { id: id3, created: true };
54786
55030
  }
54787
55031
  function readPlanSummary(ctx) {
@@ -54985,7 +55229,7 @@ function addIdeaStub(ctx) {
54985
55229
  const tags = args["tags"] ?? [];
54986
55230
  const category = args["category"] ?? "General";
54987
55231
  const id3 = `${nextAdrId(ctx)}-${slugify3(title)}`;
54988
- const path91 = workspaceArtifact(ctx.rootDir, "decisions", id3);
55232
+ const path92 = workspaceArtifact(ctx.rootDir, "decisions", id3);
54989
55233
  const meta3 = {
54990
55234
  kind: "adr",
54991
55235
  status: "proposed",
@@ -55011,7 +55255,7 @@ function addIdeaStub(ctx) {
55011
55255
  ...consequences.map((c) => `- ${c}`),
55012
55256
  ""
55013
55257
  ].join("\n");
55014
- ctx.storage.write(path91, meta3, body);
55258
+ ctx.storage.write(path92, meta3, body);
55015
55259
  return `ADR ${id3} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
55016
55260
  });
55017
55261
  }
@@ -55093,14 +55337,14 @@ function createDocumentStub(ctx) {
55093
55337
  ctx.storage.write(risksPath, riskMeta, content);
55094
55338
  return `Document "${title}" created at risks.md (workspace root).`;
55095
55339
  }
55096
- const path91 = workspaceArtifact(ctx.rootDir, "docs", slug);
55340
+ const path92 = workspaceArtifact(ctx.rootDir, "docs", slug);
55097
55341
  const meta3 = {
55098
55342
  kind: "doc",
55099
55343
  id: slug,
55100
55344
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
55101
55345
  tags
55102
55346
  };
55103
- ctx.storage.write(path91, meta3, content);
55347
+ ctx.storage.write(path92, meta3, content);
55104
55348
  return `Document "${title}" created at docs/${slug}.md.`;
55105
55349
  });
55106
55350
  }
@@ -55726,10 +55970,10 @@ function getUserMcpPath() {
55726
55970
  function getProjectMcpPath(projectRoot) {
55727
55971
  return join32(projectRoot, ".zelari", "mcp.json");
55728
55972
  }
55729
- function readFile6(path91) {
55730
- if (!existsSync37(path91)) return {};
55973
+ function readFile6(path92) {
55974
+ if (!existsSync37(path92)) return {};
55731
55975
  try {
55732
- const parsed = JSON.parse(readFileSync29(path91, "utf8"));
55976
+ const parsed = JSON.parse(readFileSync29(path92, "utf8"));
55733
55977
  const out = {};
55734
55978
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
55735
55979
  const hasCommand = !!cfg && typeof cfg.command === "string" && !!cfg.command.trim();
@@ -55751,10 +55995,10 @@ function readFile6(path91) {
55751
55995
  return {};
55752
55996
  }
55753
55997
  }
55754
- function writeFile2(path91, servers) {
55755
- mkdirSync16(dirname9(path91), { recursive: true });
55998
+ function writeFile2(path92, servers) {
55999
+ mkdirSync16(dirname9(path92), { recursive: true });
55756
56000
  const body = { mcpServers: servers };
55757
- writeFileSync18(path91, `${JSON.stringify(body, null, 2)}
56001
+ writeFileSync18(path92, `${JSON.stringify(body, null, 2)}
55758
56002
  `, "utf8");
55759
56003
  }
55760
56004
  function listMcpServers(projectRoot) {
@@ -55792,9 +56036,9 @@ function upsertMcpServer(opts) {
55792
56036
  error: "either command (stdio) or url (http) is required"
55793
56037
  };
55794
56038
  }
55795
- let path91;
56039
+ let path92;
55796
56040
  if (opts.scope === "user") {
55797
- path91 = getUserMcpPath();
56041
+ path92 = getUserMcpPath();
55798
56042
  } else {
55799
56043
  const root = opts.projectRoot?.trim();
55800
56044
  if (!root) {
@@ -55803,9 +56047,9 @@ function upsertMcpServer(opts) {
55803
56047
  error: "projectRoot required for project scope (Open Folder first)"
55804
56048
  };
55805
56049
  }
55806
- path91 = getProjectMcpPath(root);
56050
+ path92 = getProjectMcpPath(root);
55807
56051
  }
55808
- const current = readFile6(path91);
56052
+ const current = readFile6(path92);
55809
56053
  current[name] = {
55810
56054
  command: hasCommand ? opts.config.command.trim() : void 0,
55811
56055
  args: opts.config.args,
@@ -55816,21 +56060,21 @@ function upsertMcpServer(opts) {
55816
56060
  serial: opts.config.serial,
55817
56061
  enabled: opts.config.enabled !== false
55818
56062
  };
55819
- writeFile2(path91, current);
55820
- return { ok: true, path: path91 };
56063
+ writeFile2(path92, current);
56064
+ return { ok: true, path: path92 };
55821
56065
  }
55822
56066
  function removeMcpServer(opts) {
55823
- const path91 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
55824
- if (!path91) {
56067
+ const path92 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
56068
+ if (!path92) {
55825
56069
  return { ok: false, error: "projectRoot required for project scope" };
55826
56070
  }
55827
- const current = readFile6(path91);
56071
+ const current = readFile6(path92);
55828
56072
  if (!(opts.name in current)) {
55829
- return { ok: false, error: `Server "${opts.name}" not found in ${path91}` };
56073
+ return { ok: false, error: `Server "${opts.name}" not found in ${path92}` };
55830
56074
  }
55831
56075
  delete current[opts.name];
55832
- writeFile2(path91, current);
55833
- return { ok: true, path: path91 };
56076
+ writeFile2(path92, current);
56077
+ return { ok: true, path: path92 };
55834
56078
  }
55835
56079
  var init_mcpConfigIo = __esm({
55836
56080
  "src/cli/mcp/mcpConfigIo.ts"() {
@@ -56299,10 +56543,10 @@ import { createHash as createHash20 } from "node:crypto";
56299
56543
  import { join as join34 } from "node:path";
56300
56544
  import { readFile as readFile7 } from "node:fs/promises";
56301
56545
  async function readPackageJson3(projectRoot) {
56302
- const path91 = join34(projectRoot, "package.json");
56303
- if (!existsSync39(path91)) return null;
56546
+ const path92 = join34(projectRoot, "package.json");
56547
+ if (!existsSync39(path92)) return null;
56304
56548
  try {
56305
- return JSON.parse(await readFile7(path91, "utf8"));
56549
+ return JSON.parse(await readFile7(path92, "utf8"));
56306
56550
  } catch {
56307
56551
  return null;
56308
56552
  }
@@ -56384,9 +56628,9 @@ async function genBuild(ctx) {
56384
56628
  ].join("\n");
56385
56629
  }
56386
56630
  async function genOpenQuestions(ctx) {
56387
- const path91 = join34(ctx.rootDir, "risks.md");
56388
- if (!existsSync39(path91)) return "_No open questions._";
56389
- const content = readFileSync31(path91, "utf8");
56631
+ const path92 = join34(ctx.rootDir, "risks.md");
56632
+ if (!existsSync39(path92)) return "_No open questions._";
56633
+ const content = readFileSync31(path92, "utf8");
56390
56634
  const lines = content.split("\n");
56391
56635
  const questions = [];
56392
56636
  let currentTitle = "";
@@ -56660,9 +56904,9 @@ function versionKey(value) {
56660
56904
  function firstString2(v) {
56661
56905
  return typeof v === "string" && v.trim().length > 0 ? v : null;
56662
56906
  }
56663
- function readFileSyncSafe(path91) {
56907
+ function readFileSyncSafe(path92) {
56664
56908
  try {
56665
- return readFileSync32(path91, "utf8");
56909
+ return readFileSync32(path92, "utf8");
56666
56910
  } catch {
56667
56911
  return null;
56668
56912
  }
@@ -57125,8 +57369,8 @@ async function runPostCouncilHook(ctx, options) {
57125
57369
  sources: scope.sources
57126
57370
  } : void 0
57127
57371
  });
57128
- const path91 = writeCouncilCompletion(ctx.rootDir, completion);
57129
- completionHook = { ran: true, path: path91, completion };
57372
+ const path92 = writeCouncilCompletion(ctx.rootDir, completion);
57373
+ completionHook = { ran: true, path: path92, completion };
57130
57374
  } catch (err) {
57131
57375
  completionHook = {
57132
57376
  ran: true,
@@ -58462,7 +58706,7 @@ function safeSocketPath(socketPath) {
58462
58706
  return socketPath.trim();
58463
58707
  }
58464
58708
  function startPermissionBroker(socketPath, handlers, opts) {
58465
- const path91 = safeSocketPath(socketPath);
58709
+ const path92 = safeSocketPath(socketPath);
58466
58710
  const requestTimeoutMs = opts?.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
58467
58711
  const sockets = /* @__PURE__ */ new Set();
58468
58712
  const server = createServer2((socket) => {
@@ -58562,10 +58806,10 @@ function startPermissionBroker(socketPath, handlers, opts) {
58562
58806
  return new Promise((resolve9, reject) => {
58563
58807
  const onError = (err) => reject(err);
58564
58808
  server.once("error", onError);
58565
- server.listen(path91, () => {
58809
+ server.listen(path92, () => {
58566
58810
  server.removeListener("error", onError);
58567
58811
  resolve9({
58568
- socketPath: path91,
58812
+ socketPath: path92,
58569
58813
  stop: () => new Promise((res) => {
58570
58814
  for (const s of sockets) s.destroy();
58571
58815
  sockets.clear();
@@ -58576,7 +58820,7 @@ function startPermissionBroker(socketPath, handlers, opts) {
58576
58820
  if (done) return;
58577
58821
  done = true;
58578
58822
  if (process.platform !== "win32") {
58579
- unlink(path91, () => res());
58823
+ unlink(path92, () => res());
58580
58824
  } else {
58581
58825
  res();
58582
58826
  }
@@ -58589,11 +58833,11 @@ function startPermissionBroker(socketPath, handlers, opts) {
58589
58833
  });
58590
58834
  }
58591
58835
  function requestBrokerAsk(socketPath, ask, opts) {
58592
- const path91 = safeSocketPath(socketPath);
58836
+ const path92 = safeSocketPath(socketPath);
58593
58837
  const requestTimeoutMs = opts?.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
58594
58838
  const connectTimeoutMs = opts?.connectTimeoutMs ?? PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS;
58595
58839
  return new Promise((resolve9, reject) => {
58596
- const socket = connect(path91);
58840
+ const socket = connect(path92);
58597
58841
  let buffer = "";
58598
58842
  let settled = false;
58599
58843
  const settle = (fn) => {
@@ -58608,7 +58852,7 @@ function requestBrokerAsk(socketPath, ask, opts) {
58608
58852
  settle(
58609
58853
  () => reject(
58610
58854
  new Error(
58611
- `permission broker unavailable at "${path91}" (connect timed out after ${connectTimeoutMs}ms)`
58855
+ `permission broker unavailable at "${path92}" (connect timed out after ${connectTimeoutMs}ms)`
58612
58856
  )
58613
58857
  )
58614
58858
  );
@@ -62338,7 +62582,7 @@ var MIN_NODE_MAJOR, STANDARD_BASH_PATHS2, POWERSHELL_EXES2, STANDARD_POWERSHELL_
62338
62582
  var init_prereqChecks = __esm({
62339
62583
  "src/cli/utils/prereqChecks.ts"() {
62340
62584
  "use strict";
62341
- MIN_NODE_MAJOR = 20;
62585
+ MIN_NODE_MAJOR = 24;
62342
62586
  STANDARD_BASH_PATHS2 = [
62343
62587
  "C:\\Program Files\\Git\\bin\\bash.exe",
62344
62588
  "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
@@ -64426,7 +64670,7 @@ async function loadExtensionsFromDirs(dirs, options = {}) {
64426
64670
  skipped
64427
64671
  };
64428
64672
  const fsRoot = options.fsRoot ?? process.cwd();
64429
- let fs45 = null;
64673
+ let fs46 = null;
64430
64674
  const pending = [];
64431
64675
  for (const dir of dirs) {
64432
64676
  const files = candidateFiles(dir.path);
@@ -64481,9 +64725,9 @@ async function loadExtensionsFromDirs(dirs, options = {}) {
64481
64725
  skipped.push(`${file2}: missing ZelariExtension export`);
64482
64726
  continue;
64483
64727
  }
64484
- if (!fs45) fs45 = bindSandboxedFs(fsRoot);
64728
+ if (!fs46) fs46 = bindSandboxedFs(fsRoot);
64485
64729
  try {
64486
- await runtime.registry.registerExtension(ext, { fs: fs45 });
64730
+ await runtime.registry.registerExtension(ext, { fs: fs46 });
64487
64731
  runtime.loaded.push({ id: ext.id, file: file2, scope });
64488
64732
  } catch (err) {
64489
64733
  const msg = err instanceof Error ? err.message : String(err);
@@ -67797,7 +68041,7 @@ function upsertSkill(opts) {
67797
68041
  }
67798
68042
  dir = getProjectSkillsDir(root);
67799
68043
  }
67800
- const path91 = skillFilePath(dir, name);
68044
+ const path92 = skillFilePath(dir, name);
67801
68045
  const content = serializeSkillMd({
67802
68046
  name,
67803
68047
  description,
@@ -67806,13 +68050,13 @@ function upsertSkill(opts) {
67806
68050
  tools: opts.tools,
67807
68051
  cost: opts.cost
67808
68052
  });
67809
- const parsed = parseSkillMd(content, path91);
68053
+ const parsed = parseSkillMd(content, path92);
67810
68054
  if (!parsed) {
67811
68055
  return { ok: false, error: "Generated SKILL.md failed validation" };
67812
68056
  }
67813
- mkdirSync21(dirname13(path91), { recursive: true });
67814
- writeFileSync24(path91, content, "utf8");
67815
- return { ok: true, path: path91 };
68057
+ mkdirSync21(dirname13(path92), { recursive: true });
68058
+ writeFileSync24(path92, content, "utf8");
68059
+ return { ok: true, path: path92 };
67816
68060
  }
67817
68061
  function removeSkill(opts) {
67818
68062
  const name = opts.name.trim().toLowerCase();
@@ -67830,8 +68074,8 @@ function removeSkill(opts) {
67830
68074
  dir = getProjectSkillsDir(root);
67831
68075
  }
67832
68076
  const skillDir = join46(dir, name);
67833
- const path91 = skillFilePath(dir, name);
67834
- if (!existsSync52(path91) && !existsSync52(skillDir)) {
68077
+ const path92 = skillFilePath(dir, name);
68078
+ if (!existsSync52(path92) && !existsSync52(skillDir)) {
67835
68079
  return { ok: false, error: `Skill "${name}" not found in ${dir}` };
67836
68080
  }
67837
68081
  try {
@@ -67842,7 +68086,7 @@ function removeSkill(opts) {
67842
68086
  error: err instanceof Error ? err.message : String(err)
67843
68087
  };
67844
68088
  }
67845
- return { ok: true, path: path91 };
68089
+ return { ok: true, path: path92 };
67846
68090
  }
67847
68091
  var NAME_RE, BUILTIN_SKILL_MODULES, builtinsLoaded;
67848
68092
  var init_skillConfigIo = __esm({
@@ -68661,12 +68905,12 @@ function ensureHome() {
68661
68905
  }
68662
68906
  }
68663
68907
  function loadCompanionConfig() {
68664
- const path91 = getCompanionConfigPath();
68665
- if (!existsSync53(path91)) {
68908
+ const path92 = getCompanionConfigPath();
68909
+ if (!existsSync53(path92)) {
68666
68910
  return { projects: [] };
68667
68911
  }
68668
68912
  try {
68669
- const raw = JSON.parse(readFileSync41(path91, "utf8"));
68913
+ const raw = JSON.parse(readFileSync41(path92, "utf8"));
68670
68914
  const projects = Array.isArray(raw.projects) ? raw.projects.filter(
68671
68915
  (p3) => p3 && typeof p3.path === "string" && p3.path.trim() && typeof (p3.id ?? p3.name) === "string"
68672
68916
  ).map((p3) => ({
@@ -68704,16 +68948,16 @@ function loadOrCreateToken(explicit) {
68704
68948
  return { token: explicit.trim(), created: false };
68705
68949
  }
68706
68950
  ensureHome();
68707
- const path91 = getCompanionTokenPath();
68708
- if (existsSync53(path91)) {
68709
- const t = readFileSync41(path91, "utf8").trim();
68951
+ const path92 = getCompanionTokenPath();
68952
+ if (existsSync53(path92)) {
68953
+ const t = readFileSync41(path92, "utf8").trim();
68710
68954
  if (t) return { token: t, created: false };
68711
68955
  }
68712
68956
  const token = randomBytes7(24).toString("base64url");
68713
- writeFileSync25(path91, token + "\n", "utf8");
68957
+ writeFileSync25(path92, token + "\n", "utf8");
68714
68958
  try {
68715
- const fs45 = __require("node:fs");
68716
- fs45.chmodSync?.(path91, 384);
68959
+ const fs46 = __require("node:fs");
68960
+ fs46.chmodSync?.(path92, 384);
68717
68961
  } catch {
68718
68962
  }
68719
68963
  return { token, created: true };
@@ -68738,17 +68982,17 @@ function mergeProjects(cfg, extraPaths) {
68738
68982
  byId.set(p3.id, p3);
68739
68983
  }
68740
68984
  for (const raw of extraPaths) {
68741
- const path91 = raw.trim();
68742
- if (!path91) continue;
68743
- let id3 = slugFromPath(path91);
68985
+ const path92 = raw.trim();
68986
+ if (!path92) continue;
68987
+ let id3 = slugFromPath(path92);
68744
68988
  let n = 2;
68745
- while (byId.has(id3) && byId.get(id3).path !== path91) {
68746
- id3 = `${slugFromPath(path91)}-${n++}`;
68989
+ while (byId.has(id3) && byId.get(id3).path !== path92) {
68990
+ id3 = `${slugFromPath(path92)}-${n++}`;
68747
68991
  }
68748
68992
  byId.set(id3, {
68749
68993
  id: id3,
68750
- name: slugFromPath(path91),
68751
- path: path91
68994
+ name: slugFromPath(path92),
68995
+ path: path92
68752
68996
  });
68753
68997
  }
68754
68998
  return [...byId.values()];
@@ -68795,6 +69039,63 @@ var init_config = __esm({
68795
69039
  }
68796
69040
  });
68797
69041
 
69042
+ // src/cli/serve/spineLockSweep.ts
69043
+ import { promises as fs45 } from "node:fs";
69044
+ import path88 from "node:path";
69045
+ async function sweepOrphanSpineLocks(sessionsDir, options = {}) {
69046
+ const dir = sessionsDir ?? resolveSessionsDir();
69047
+ const onSwept = options.onSwept ?? ((sessionId2, reason) => {
69048
+ console.error(
69049
+ `[zelari] spine-lock sweep: removed orphan writer.lock for session ${sessionId2} (${reason})`
69050
+ );
69051
+ });
69052
+ const now = options.now ?? Date.now;
69053
+ const probe = options.probe ?? defaultLockLivenessProbe;
69054
+ const result = { swept: 0, kept: 0, errors: 0 };
69055
+ let entries;
69056
+ try {
69057
+ entries = await fs45.readdir(dir);
69058
+ } catch {
69059
+ return result;
69060
+ }
69061
+ for (const sessionId2 of entries) {
69062
+ const lockPath2 = path88.join(dir, sessionId2, "writer.lock");
69063
+ try {
69064
+ const raw = await fs45.readFile(lockPath2, "utf-8");
69065
+ let lockInfo = {};
69066
+ try {
69067
+ lockInfo = JSON.parse(raw);
69068
+ } catch {
69069
+ result.kept += 1;
69070
+ continue;
69071
+ }
69072
+ const verdict = evaluateLockTakeover(lockInfo, {
69073
+ now: now(),
69074
+ probe,
69075
+ heartbeatStaleMs: options.heartbeatStaleMs ?? resolveHeartbeatStaleMs()
69076
+ });
69077
+ if (!verdict.takeover) {
69078
+ result.kept += 1;
69079
+ continue;
69080
+ }
69081
+ await fs45.rm(lockPath2, { force: true });
69082
+ result.swept += 1;
69083
+ onSwept(sessionId2, verdict.reason);
69084
+ } catch (err) {
69085
+ const code = err.code;
69086
+ if (code === "ENOENT" || code === "ENOTDIR") continue;
69087
+ result.errors += 1;
69088
+ }
69089
+ }
69090
+ return result;
69091
+ }
69092
+ var init_spineLockSweep = __esm({
69093
+ "src/cli/serve/spineLockSweep.ts"() {
69094
+ "use strict";
69095
+ init_session();
69096
+ }
69097
+ });
69098
+
68798
69099
  // src/cli/serve/harnessServer.ts
68799
69100
  var harnessServer_exports = {};
68800
69101
  __export(harnessServer_exports, {
@@ -69042,6 +69343,10 @@ function startHarnessServer(options = {}) {
69042
69343
  async function runHarnessServer() {
69043
69344
  setActivePolicyLoadSurface("headless");
69044
69345
  setActiveProofPersistenceSurface("headless");
69346
+ try {
69347
+ await sweepOrphanSpineLocks();
69348
+ } catch {
69349
+ }
69045
69350
  process.env.ZELARI_SERVE_HARNESS = "1";
69046
69351
  const started = startHarnessServer();
69047
69352
  await new Promise((resolvePromise) => {
@@ -69066,6 +69371,7 @@ var init_harnessServer = __esm({
69066
69371
  init_completionProofPersist();
69067
69372
  init_policyGate();
69068
69373
  init_policyLoadMode();
69374
+ init_spineLockSweep();
69069
69375
  }
69070
69376
  });
69071
69377
 
@@ -69723,9 +70029,9 @@ async function runCompanionServe(opts = {}) {
69723
70029
  return;
69724
70030
  }
69725
70031
  const url2 = parseUrl(req);
69726
- const path91 = url2.pathname.replace(/\/+$/, "") || "/";
70032
+ const path92 = url2.pathname.replace(/\/+$/, "") || "/";
69727
70033
  try {
69728
- if (req.method === "GET" && (path91 === "/health" || path91 === "/v1/health")) {
70034
+ if (req.method === "GET" && (path92 === "/health" || path92 === "/v1/health")) {
69729
70035
  sendJson2(res, 200, {
69730
70036
  ok: true,
69731
70037
  service: "zelari-companion",
@@ -69737,18 +70043,18 @@ async function runCompanionServe(opts = {}) {
69737
70043
  });
69738
70044
  return;
69739
70045
  }
69740
- if (path91.startsWith("/v1")) {
70046
+ if (path92.startsWith("/v1")) {
69741
70047
  if (!tokenMatches(token, getBearer(req))) {
69742
70048
  sendJson2(res, 401, { ok: false, error: "unauthorized" });
69743
70049
  return;
69744
70050
  }
69745
70051
  }
69746
- if (req.method === "GET" && path91 === "/v1/config") {
70052
+ if (req.method === "GET" && path92 === "/v1/config") {
69747
70053
  const snap = buildDesktopConfigSnapshot();
69748
70054
  sendJson2(res, 200, { ok: true, ...snap });
69749
70055
  return;
69750
70056
  }
69751
- if (req.method === "GET" && path91 === "/v1/projects") {
70057
+ if (req.method === "GET" && path92 === "/v1/projects") {
69752
70058
  sendJson2(res, 200, {
69753
70059
  ok: true,
69754
70060
  projects: projects.map((p3) => ({
@@ -69759,7 +70065,7 @@ async function runCompanionServe(opts = {}) {
69759
70065
  });
69760
70066
  return;
69761
70067
  }
69762
- if (req.method === "GET" && path91 === "/v1/runs") {
70068
+ if (req.method === "GET" && path92 === "/v1/runs") {
69763
70069
  sendJson2(res, 200, {
69764
70070
  ok: true,
69765
70071
  active: runs.getActive(),
@@ -69777,7 +70083,7 @@ async function runCompanionServe(opts = {}) {
69777
70083
  });
69778
70084
  return;
69779
70085
  }
69780
- if (req.method === "POST" && path91 === "/v1/runs") {
70086
+ if (req.method === "POST" && path92 === "/v1/runs") {
69781
70087
  const raw = await readBody(req);
69782
70088
  let body = {};
69783
70089
  try {
@@ -69825,7 +70131,7 @@ async function runCompanionServe(opts = {}) {
69825
70131
  });
69826
70132
  return;
69827
70133
  }
69828
- const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path91);
70134
+ const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path92);
69829
70135
  if (req.method === "GET" && eventsMatch) {
69830
70136
  const runId = eventsMatch[1];
69831
70137
  const run = runs.getRun(runId);
@@ -69890,7 +70196,7 @@ async function runCompanionServe(opts = {}) {
69890
70196
  }, 500);
69891
70197
  return;
69892
70198
  }
69893
- const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path91);
70199
+ const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path92);
69894
70200
  if (req.method === "POST" && cancelMatch) {
69895
70201
  const runId = cancelMatch[1];
69896
70202
  const result = runs.cancel(runId);
@@ -69901,7 +70207,7 @@ async function runCompanionServe(opts = {}) {
69901
70207
  sendJson2(res, 200, { ok: true, cancelled: runId });
69902
70208
  return;
69903
70209
  }
69904
- const steerMatch = /^\/v1\/runs\/([^/]+)\/steer$/.exec(path91);
70210
+ const steerMatch = /^\/v1\/runs\/([^/]+)\/steer$/.exec(path92);
69905
70211
  if (req.method === "POST" && steerMatch) {
69906
70212
  const runId = steerMatch[1];
69907
70213
  const raw = await readBody(req);
@@ -70068,11 +70374,11 @@ import { execSync as execSync2 } from "node:child_process";
70068
70374
  import { existsSync as existsSync55, readFileSync as readFileSync42, readlinkSync, statSync as statSync10 } from "node:fs";
70069
70375
  import { createRequire as createRequire3 } from "node:module";
70070
70376
  import { fileURLToPath as fileURLToPath3 } from "node:url";
70071
- import path88 from "node:path";
70377
+ import path89 from "node:path";
70072
70378
  function findPackageRoot(start) {
70073
70379
  let dir = start;
70074
70380
  for (let i = 0; i < 6; i += 1) {
70075
- const candidate = path88.join(dir, "package.json");
70381
+ const candidate = path89.join(dir, "package.json");
70076
70382
  if (existsSync55(candidate)) {
70077
70383
  try {
70078
70384
  const pkg = JSON.parse(readFileSync42(candidate, "utf8"));
@@ -70080,11 +70386,11 @@ function findPackageRoot(start) {
70080
70386
  } catch {
70081
70387
  }
70082
70388
  }
70083
- const parent = path88.dirname(dir);
70389
+ const parent = path89.dirname(dir);
70084
70390
  if (parent === dir) break;
70085
70391
  dir = parent;
70086
70392
  }
70087
- return path88.resolve(__dirname3, "..", "..", "..");
70393
+ return path89.resolve(__dirname3, "..", "..", "..");
70088
70394
  }
70089
70395
  function tryExec(cmd) {
70090
70396
  try {
@@ -70098,7 +70404,7 @@ function tryExec(cmd) {
70098
70404
  }
70099
70405
  function readPackageJson4() {
70100
70406
  try {
70101
- const pkgPath = path88.join(packageRoot, "package.json");
70407
+ const pkgPath = path89.join(packageRoot, "package.json");
70102
70408
  return JSON.parse(readFileSync42(pkgPath, "utf8"));
70103
70409
  } catch {
70104
70410
  return null;
@@ -70114,7 +70420,7 @@ function checkShim(pkgName) {
70114
70420
  }
70115
70421
  const isWin = process.platform === "win32";
70116
70422
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
70117
- const shimPath = path88.join(prefix, shimName);
70423
+ const shimPath = path89.join(prefix, shimName);
70118
70424
  if (!existsSync55(shimPath)) {
70119
70425
  return FAIL(
70120
70426
  `shim not found at ${shimPath}
@@ -70142,8 +70448,8 @@ function checkShim(pkgName) {
70142
70448
  fix: npm install -g ${pkgName}@latest --force`
70143
70449
  );
70144
70450
  }
70145
- const resolved = path88.resolve(path88.dirname(shimPath), target);
70146
- const expected = path88.join(
70451
+ const resolved = path89.resolve(path89.dirname(shimPath), target);
70452
+ const expected = path89.join(
70147
70453
  prefix,
70148
70454
  "node_modules",
70149
70455
  pkgName,
@@ -70174,15 +70480,18 @@ function checkNode(pkg) {
70174
70480
  return WARN(`could not parse node version: ${raw}`);
70175
70481
  }
70176
70482
  const major = Number(m[1]);
70177
- if (major < 20) {
70483
+ const enginesNode = pkg?.engines?.node;
70484
+ const requiredMajor = Number(enginesNode?.match(/\d+/)?.[0] ?? 20);
70485
+ if (major < requiredMajor) {
70178
70486
  return FAIL(
70179
- `node ${raw} is older than the required engines.node (>= 20.0.0)`
70487
+ `node ${raw} is older than the required engines.node (${enginesNode ?? ">= 20.0.0"})
70488
+ fix: install Node.js ${requiredMajor}+ (LTS) and reopen the terminal`
70180
70489
  );
70181
70490
  }
70182
- return OK(`node ${raw}`);
70491
+ return OK(`node ${raw} (engines.node ${enginesNode ?? ">= 20.0.0"})`);
70183
70492
  }
70184
70493
  function checkBundle() {
70185
- const bundle = path88.join(packageRoot, "dist", "cli", "main.bundled.js");
70494
+ const bundle = path89.join(packageRoot, "dist", "cli", "main.bundled.js");
70186
70495
  if (!existsSync55(bundle)) {
70187
70496
  return FAIL(
70188
70497
  `dist/cli/main.bundled.js missing at ${bundle}
@@ -70203,7 +70512,7 @@ function checkRuntimeDeps() {
70203
70512
  const missing = [];
70204
70513
  for (const dep of required2) {
70205
70514
  try {
70206
- const localReq = createRequire3(path88.join(packageRoot, "package.json"));
70515
+ const localReq = createRequire3(path89.join(packageRoot, "package.json"));
70207
70516
  localReq.resolve(dep);
70208
70517
  } catch {
70209
70518
  missing.push(dep);
@@ -70403,7 +70712,7 @@ var init_doctor = __esm({
70403
70712
  init_metrics3();
70404
70713
  init_contextGrowthSummary();
70405
70714
  require3 = createRequire3(import.meta.url);
70406
- __dirname3 = path88.dirname(fileURLToPath3(import.meta.url));
70715
+ __dirname3 = path89.dirname(fileURLToPath3(import.meta.url));
70407
70716
  packageRoot = findPackageRoot(__dirname3);
70408
70717
  OK = (message) => ({
70409
70718
  ok: true,
@@ -70587,7 +70896,7 @@ __export(inspectSession_exports, {
70587
70896
  renderInspectReport: () => renderInspectReport,
70588
70897
  runInspectSession: () => runInspectSession
70589
70898
  });
70590
- import path89 from "node:path";
70899
+ import path90 from "node:path";
70591
70900
  import { existsSync as existsSync56 } from "node:fs";
70592
70901
  function formatLimit(limit) {
70593
70902
  return `${Math.round(limit / 1e3)}k`;
@@ -70621,8 +70930,8 @@ function renderInspectReport(state3) {
70621
70930
  }
70622
70931
  async function runInspectSession(opts) {
70623
70932
  const sessionsDir = resolveSessionsDir({ workspaceRoot: opts.cwd ?? process.cwd() });
70624
- const sessionDir = path89.join(sessionsDir, opts.sessionId);
70625
- const eventsPath = path89.join(sessionDir, "events.jsonl");
70933
+ const sessionDir = path90.join(sessionsDir, opts.sessionId);
70934
+ const eventsPath = path90.join(sessionDir, "events.jsonl");
70626
70935
  if (!existsSync56(sessionDir)) {
70627
70936
  console.error(`zelari-code inspect: no session directory at ${sessionDir}`);
70628
70937
  return 1;
@@ -70656,15 +70965,15 @@ __export(inspect_exports, {
70656
70965
  collectInspectReport: () => collectInspectReport,
70657
70966
  runInspect: () => runInspect
70658
70967
  });
70659
- import path90 from "node:path";
70968
+ import path91 from "node:path";
70660
70969
  import { existsSync as existsSync57, readFileSync as readFileSync43, readdirSync as readdirSync12 } from "node:fs";
70661
70970
  import { homedir as homedir17 } from "node:os";
70662
70971
  async function collectInspectReport(cwd = process.cwd()) {
70663
70972
  ensureBuiltinSkillsLoadedSync();
70664
70973
  const snap = listSkillsSnapshot(cwd);
70665
70974
  const mcp = listMcpServers(cwd);
70666
- const userMcpPath = path90.join(homedir17(), ".zelari-code", "mcp.json");
70667
- const projectMcpPath = path90.join(cwd, ".zelari", "mcp.json");
70975
+ const userMcpPath = path91.join(homedir17(), ".zelari-code", "mcp.json");
70976
+ const projectMcpPath = path91.join(cwd, ".zelari", "mcp.json");
70668
70977
  const globalHooks = globalHooksDir();
70669
70978
  const projectHooks = projectHooksDir(cwd);
70670
70979
  const projectTrusted = isFolderTrusted(cwd);
@@ -70692,9 +71001,9 @@ async function collectInspectReport(cwd = process.cwd()) {
70692
71001
  configSources: [
70693
71002
  { path: userMcpPath, exists: existsSync57(userMcpPath) },
70694
71003
  { path: projectMcpPath, exists: existsSync57(projectMcpPath) },
70695
- { path: path90.join(homedir17(), ".zelari-code", "provider.json"), exists: existsSync57(path90.join(homedir17(), ".zelari-code", "provider.json")) },
70696
- { path: path90.join(cwd, ".zelari", "AGENTS.md"), exists: existsSync57(path90.join(cwd, ".zelari", "AGENTS.md")) },
70697
- { path: path90.join(cwd, "AGENTS.md"), exists: existsSync57(path90.join(cwd, "AGENTS.md")) }
71004
+ { path: path91.join(homedir17(), ".zelari-code", "provider.json"), exists: existsSync57(path91.join(homedir17(), ".zelari-code", "provider.json")) },
71005
+ { path: path91.join(cwd, ".zelari", "AGENTS.md"), exists: existsSync57(path91.join(cwd, ".zelari", "AGENTS.md")) },
71006
+ { path: path91.join(cwd, "AGENTS.md"), exists: existsSync57(path91.join(cwd, "AGENTS.md")) }
70698
71007
  ],
70699
71008
  skills: {
70700
71009
  total: snap.skills.length,
@@ -70734,8 +71043,8 @@ function listJsonFiles(dir) {
70734
71043
  }
70735
71044
  function findAgentsMd(cwd) {
70736
71045
  const candidates = [
70737
- path90.join(cwd, "AGENTS.md"),
70738
- path90.join(cwd, ".zelari", "AGENTS.md")
71046
+ path91.join(cwd, "AGENTS.md"),
71047
+ path91.join(cwd, ".zelari", "AGENTS.md")
70739
71048
  ];
70740
71049
  const found = [];
70741
71050
  for (const c of candidates) {
@@ -80380,8 +80689,8 @@ function normalizeDraft(raw, sourceUrl, provider, model) {
80380
80689
  let name = String(o.name ?? "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
80381
80690
  if (!name || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
80382
80691
  try {
80383
- const path91 = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop()?.replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
80384
- name = path91 && /^[a-z0-9]/.test(path91) ? path91 : "imported-skill";
80692
+ const path92 = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop()?.replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
80693
+ name = path92 && /^[a-z0-9]/.test(path92) ? path92 : "imported-skill";
80385
80694
  } catch {
80386
80695
  name = "imported-skill";
80387
80696
  }