zelari-code 1.27.2 → 1.29.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.
@@ -1829,10 +1829,10 @@ function mergeDefs(...defs) {
1829
1829
  function cloneDef(schema) {
1830
1830
  return mergeDefs(schema._zod.def);
1831
1831
  }
1832
- function getElementAtPath(obj, path43) {
1833
- if (!path43)
1832
+ function getElementAtPath(obj, path44) {
1833
+ if (!path44)
1834
1834
  return obj;
1835
- return path43.reduce((acc, key) => acc?.[key], obj);
1835
+ return path44.reduce((acc, key) => acc?.[key], obj);
1836
1836
  }
1837
1837
  function promiseAllObject(promisesObj) {
1838
1838
  const keys = Object.keys(promisesObj);
@@ -2160,11 +2160,11 @@ function explicitlyAborted(x, startIndex = 0) {
2160
2160
  }
2161
2161
  return false;
2162
2162
  }
2163
- function prefixIssues(path43, issues) {
2163
+ function prefixIssues(path44, issues) {
2164
2164
  return issues.map((iss) => {
2165
2165
  var _a3;
2166
2166
  (_a3 = iss).path ?? (_a3.path = []);
2167
- iss.path.unshift(path43);
2167
+ iss.path.unshift(path44);
2168
2168
  return iss;
2169
2169
  });
2170
2170
  }
@@ -2382,16 +2382,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2382
2382
  }
2383
2383
  function formatError(error51, mapper = (issue2) => issue2.message) {
2384
2384
  const fieldErrors = { _errors: [] };
2385
- const processError = (error52, path43 = []) => {
2385
+ const processError = (error52, path44 = []) => {
2386
2386
  for (const issue2 of error52.issues) {
2387
2387
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2388
- issue2.errors.map((issues) => processError({ issues }, [...path43, ...issue2.path]));
2388
+ issue2.errors.map((issues) => processError({ issues }, [...path44, ...issue2.path]));
2389
2389
  } else if (issue2.code === "invalid_key") {
2390
- processError({ issues: issue2.issues }, [...path43, ...issue2.path]);
2390
+ processError({ issues: issue2.issues }, [...path44, ...issue2.path]);
2391
2391
  } else if (issue2.code === "invalid_element") {
2392
- processError({ issues: issue2.issues }, [...path43, ...issue2.path]);
2392
+ processError({ issues: issue2.issues }, [...path44, ...issue2.path]);
2393
2393
  } else {
2394
- const fullpath = [...path43, ...issue2.path];
2394
+ const fullpath = [...path44, ...issue2.path];
2395
2395
  if (fullpath.length === 0) {
2396
2396
  fieldErrors._errors.push(mapper(issue2));
2397
2397
  } else {
@@ -2418,17 +2418,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2418
2418
  }
2419
2419
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2420
2420
  const result = { errors: [] };
2421
- const processError = (error52, path43 = []) => {
2421
+ const processError = (error52, path44 = []) => {
2422
2422
  var _a3, _b;
2423
2423
  for (const issue2 of error52.issues) {
2424
2424
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2425
- issue2.errors.map((issues) => processError({ issues }, [...path43, ...issue2.path]));
2425
+ issue2.errors.map((issues) => processError({ issues }, [...path44, ...issue2.path]));
2426
2426
  } else if (issue2.code === "invalid_key") {
2427
- processError({ issues: issue2.issues }, [...path43, ...issue2.path]);
2427
+ processError({ issues: issue2.issues }, [...path44, ...issue2.path]);
2428
2428
  } else if (issue2.code === "invalid_element") {
2429
- processError({ issues: issue2.issues }, [...path43, ...issue2.path]);
2429
+ processError({ issues: issue2.issues }, [...path44, ...issue2.path]);
2430
2430
  } else {
2431
- const fullpath = [...path43, ...issue2.path];
2431
+ const fullpath = [...path44, ...issue2.path];
2432
2432
  if (fullpath.length === 0) {
2433
2433
  result.errors.push(mapper(issue2));
2434
2434
  continue;
@@ -2460,8 +2460,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2460
2460
  }
2461
2461
  function toDotPath(_path) {
2462
2462
  const segs = [];
2463
- const path43 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2464
- for (const seg of path43) {
2463
+ const path44 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2464
+ for (const seg of path44) {
2465
2465
  if (typeof seg === "number")
2466
2466
  segs.push(`[${seg}]`);
2467
2467
  else if (typeof seg === "symbol")
@@ -15964,13 +15964,13 @@ function resolveRef(ref, ctx) {
15964
15964
  if (!ref.startsWith("#")) {
15965
15965
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15966
15966
  }
15967
- const path43 = ref.slice(1).split("/").filter(Boolean);
15968
- if (path43.length === 0) {
15967
+ const path44 = ref.slice(1).split("/").filter(Boolean);
15968
+ if (path44.length === 0) {
15969
15969
  return ctx.rootSchema;
15970
15970
  }
15971
15971
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15972
- if (path43[0] === defsKey) {
15973
- const key = path43[1];
15972
+ if (path44[0] === defsKey) {
15973
+ const key = path44[1];
15974
15974
  if (!key || !ctx.defs[key]) {
15975
15975
  throw new Error(`Reference not found: ${ref}`);
15976
15976
  }
@@ -18134,11 +18134,11 @@ var init_tools = __esm({
18134
18134
  if (!ctx.addDocument)
18135
18135
  return "Knowledge vault tool not available.";
18136
18136
  const title = args["title"] || "New Document";
18137
- const path43 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
18137
+ const path44 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
18138
18138
  const content = args["content"] || "";
18139
18139
  const tags = args["tags"] || [];
18140
18140
  ctx.addDocument({
18141
- path: path43,
18141
+ path: path44,
18142
18142
  title,
18143
18143
  content,
18144
18144
  format: "markdown",
@@ -18147,7 +18147,7 @@ var init_tools = __esm({
18147
18147
  workspaceId: ctx.workspaceId
18148
18148
  });
18149
18149
  ctx.addActivity("vault", "created document", title);
18150
- return `Document "${title}" created at "${path43}".`;
18150
+ return `Document "${title}" created at "${path44}".`;
18151
18151
  }
18152
18152
  }
18153
18153
  ];
@@ -21839,11 +21839,11 @@ var init_synthesisAudit = __esm({
21839
21839
  import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
21840
21840
  import { join as join2 } from "node:path";
21841
21841
  function loadNfrSpec(zelariRoot) {
21842
- const path43 = join2(zelariRoot, "nfr-spec.json");
21843
- if (!existsSync6(path43))
21842
+ const path44 = join2(zelariRoot, "nfr-spec.json");
21843
+ if (!existsSync6(path44))
21844
21844
  return null;
21845
21845
  try {
21846
- const raw = JSON.parse(readFileSync5(path43, "utf8"));
21846
+ const raw = JSON.parse(readFileSync5(path44, "utf8"));
21847
21847
  if (raw.version !== 1 || !Array.isArray(raw.targets))
21848
21848
  return null;
21849
21849
  return raw;
@@ -24149,9 +24149,9 @@ var init_types3 = __esm({
24149
24149
  import { readFileSync as readFileSync10 } from "node:fs";
24150
24150
  import { join as join8 } from "node:path";
24151
24151
  function readLessonsDeduped(zelariRoot) {
24152
- const path43 = join8(zelariRoot, LESSONS_FILE);
24152
+ const path44 = join8(zelariRoot, LESSONS_FILE);
24153
24153
  try {
24154
- const raw = readFileSync10(path43, "utf8");
24154
+ const raw = readFileSync10(path44, "utf8");
24155
24155
  const byId = /* @__PURE__ */ new Map();
24156
24156
  for (const line of raw.split(/\r?\n/)) {
24157
24157
  if (!line.trim())
@@ -24252,8 +24252,8 @@ function keywordsFrom(check2, signature) {
24252
24252
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
24253
24253
  }
24254
24254
  function writeLesson(zelariRoot, lesson) {
24255
- const path43 = join9(zelariRoot, LESSONS_FILE);
24256
- appendFileSync(path43, `${JSON.stringify(lesson)}
24255
+ const path44 = join9(zelariRoot, LESSONS_FILE);
24256
+ appendFileSync(path44, `${JSON.stringify(lesson)}
24257
24257
  `, "utf8");
24258
24258
  }
24259
24259
  function findSimilar(lessons, signature) {
@@ -24866,9 +24866,9 @@ function findCycle(nodes) {
24866
24866
  if (color.get(start) !== WHITE)
24867
24867
  continue;
24868
24868
  const stack = [[start, 0]];
24869
- const path43 = [];
24869
+ const path44 = [];
24870
24870
  color.set(start, GRAY);
24871
- path43.push(start);
24871
+ path44.push(start);
24872
24872
  while (stack.length > 0) {
24873
24873
  const top = stack[stack.length - 1];
24874
24874
  const [id, idx] = top;
@@ -24881,17 +24881,17 @@ function findCycle(nodes) {
24881
24881
  continue;
24882
24882
  const c = color.get(dep);
24883
24883
  if (c === GRAY) {
24884
- const at = path43.indexOf(dep);
24885
- return [...path43.slice(at), dep];
24884
+ const at = path44.indexOf(dep);
24885
+ return [...path44.slice(at), dep];
24886
24886
  }
24887
24887
  if (c === WHITE) {
24888
24888
  color.set(dep, GRAY);
24889
- path43.push(dep);
24889
+ path44.push(dep);
24890
24890
  stack.push([dep, 0]);
24891
24891
  }
24892
24892
  } else {
24893
24893
  color.set(id, BLACK);
24894
- path43.pop();
24894
+ path44.pop();
24895
24895
  stack.pop();
24896
24896
  }
24897
24897
  }
@@ -25074,14 +25074,6 @@ function canRunParallel(a, b) {
25074
25074
  }
25075
25075
  return false;
25076
25076
  }
25077
- function selectParallelWave(candidates) {
25078
- const wave = [];
25079
- for (const node of candidates) {
25080
- if (wave.every((w) => canRunParallel(w, node)))
25081
- wave.push(node);
25082
- }
25083
- return wave;
25084
- }
25085
25077
  var READ_ONLY_KINDS, WRITER_KINDS;
25086
25078
  var init_conflict = __esm({
25087
25079
  "packages/core/dist/kraken/conflict.js"() {
@@ -25091,12 +25083,49 @@ var init_conflict = __esm({
25091
25083
  }
25092
25084
  });
25093
25085
 
25086
+ // packages/core/dist/kraken/verdict.js
25087
+ function parseVerifyVerdict(text) {
25088
+ const source = typeof text === "string" ? text : "";
25089
+ if (source.trim() === "")
25090
+ return { verdict: "unknown", findings: "" };
25091
+ VERDICT_LINE.lastIndex = 0;
25092
+ let match;
25093
+ let last = null;
25094
+ while ((match = VERDICT_LINE.exec(source)) !== null) {
25095
+ last = match;
25096
+ if (match.index === VERDICT_LINE.lastIndex)
25097
+ VERDICT_LINE.lastIndex += 1;
25098
+ }
25099
+ if (!last) {
25100
+ return { verdict: "unknown", findings: capFindings(source) };
25101
+ }
25102
+ const verdict = last[1].toUpperCase() === "FAIL" ? "fail" : "pass";
25103
+ const findings = capFindings(source.slice(0, last.index));
25104
+ return { verdict, findings };
25105
+ }
25106
+ function capFindings(raw) {
25107
+ const trimmed = raw.trim();
25108
+ if (trimmed.length <= MAX_FINDINGS_CHARS)
25109
+ return trimmed;
25110
+ return `${trimmed.slice(0, MAX_FINDINGS_CHARS)}
25111
+ \u2026 [truncated]`;
25112
+ }
25113
+ var MAX_FINDINGS_CHARS, VERDICT_LINE;
25114
+ var init_verdict = __esm({
25115
+ "packages/core/dist/kraken/verdict.js"() {
25116
+ "use strict";
25117
+ MAX_FINDINGS_CHARS = 2800;
25118
+ VERDICT_LINE = /^[\s>*_-]*VERDICT[\s*_]*:[\s*_]*(PASS|FAIL)\b/gim;
25119
+ }
25120
+ });
25121
+
25094
25122
  // packages/core/dist/kraken/index.js
25095
25123
  var init_kraken = __esm({
25096
25124
  "packages/core/dist/kraken/index.js"() {
25097
25125
  "use strict";
25098
25126
  init_graph();
25099
25127
  init_conflict();
25128
+ init_verdict();
25100
25129
  }
25101
25130
  });
25102
25131
 
@@ -25118,8 +25147,11 @@ var init_dist = __esm({
25118
25147
  // src/cli/kraken/graphStatus.ts
25119
25148
  var graphStatus_exports = {};
25120
25149
  __export(graphStatus_exports, {
25150
+ DEFAULT_DIGEST_RESULT_CHARS: () => DEFAULT_DIGEST_RESULT_CHARS,
25121
25151
  endKrakenGraphLive: () => endKrakenGraphLive,
25152
+ formatDuration: () => formatDuration2,
25122
25153
  formatKrakenGraphAscii: () => formatKrakenGraphAscii,
25154
+ formatKrakenGraphDigest: () => formatKrakenGraphDigest,
25123
25155
  formatKrakenGraphSummary: () => formatKrakenGraphSummary,
25124
25156
  getKrakenGraphLive: () => getKrakenGraphLive,
25125
25157
  resetKrakenGraphLive: () => resetKrakenGraphLive,
@@ -25147,6 +25179,46 @@ function formatKrakenGraphAscii(graph) {
25147
25179
  if (lines.length === 0) return summary;
25148
25180
  return [summary, ...lines].join("\n");
25149
25181
  }
25182
+ function formatDuration2(ms) {
25183
+ if (!Number.isFinite(ms) || ms < 0) return "?";
25184
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
25185
+ const totalSeconds = Math.round(ms / 1e3);
25186
+ if (totalSeconds < 60) return `${totalSeconds}s`;
25187
+ const m = Math.floor(totalSeconds / 60);
25188
+ const s = totalSeconds % 60;
25189
+ return s === 0 ? `${m}m` : `${m}m${s}s`;
25190
+ }
25191
+ function formatKrakenGraphDigest(graph, opts = {}) {
25192
+ const maxChars = opts.maxResultChars ?? DEFAULT_DIGEST_RESULT_CHARS;
25193
+ const durations = opts.durationsMs ?? {};
25194
+ const ordered = topoLevels(graph).flat();
25195
+ for (const id of graph.nodes.keys()) {
25196
+ if (!ordered.includes(id)) ordered.push(id);
25197
+ }
25198
+ const lines = [];
25199
+ for (const id of ordered) {
25200
+ const n = graph.nodes.get(id);
25201
+ if (!n) continue;
25202
+ const took = durations[id] !== void 0 ? `, ${formatDuration2(durations[id])}` : "";
25203
+ const detail = n.status === "error" ? n.error : n.result;
25204
+ const firstLine2 = (detail ?? "").trim().split("\n")[0] ?? "";
25205
+ const body = firstLine2.length > maxChars ? `${firstLine2.slice(0, maxChars)}\u2026` : firstLine2;
25206
+ lines.push(
25207
+ `[${STATUS_ICON[n.status]}] ${n.id} (${n.kind}${took})${body ? ` \u2014 ${body}` : ""}`
25208
+ );
25209
+ }
25210
+ const unresolved = opts.unresolvedFindings ?? [];
25211
+ if (unresolved.length > 0) {
25212
+ lines.push("", "unresolved verify findings:");
25213
+ for (const u of unresolved) {
25214
+ const why = u.reason === "fail" ? "rejected, rework budget spent" : "no parseable verdict";
25215
+ const detail = u.findings.trim().split("\n")[0]?.trim() ?? "";
25216
+ const body = detail.length > maxChars ? `${detail.slice(0, maxChars)}\u2026` : detail;
25217
+ lines.push(` ${u.nodeId} (${why})${body ? ` \u2014 ${body}` : ""}`);
25218
+ }
25219
+ }
25220
+ return lines.join("\n");
25221
+ }
25150
25222
  function state() {
25151
25223
  return globalThis;
25152
25224
  }
@@ -25195,7 +25267,7 @@ function formatKrakenGraphSummary() {
25195
25267
  if (live.error) parts.push(`${live.error}\u2717`);
25196
25268
  return `graph ${parts.join(" \xB7 ")}`;
25197
25269
  }
25198
- var STATUS_ICON;
25270
+ var STATUS_ICON, DEFAULT_DIGEST_RESULT_CHARS;
25199
25271
  var init_graphStatus = __esm({
25200
25272
  "src/cli/kraken/graphStatus.ts"() {
25201
25273
  "use strict";
@@ -25207,6 +25279,7 @@ var init_graphStatus = __esm({
25207
25279
  error: "\u2717",
25208
25280
  skipped: "\xBB"
25209
25281
  };
25282
+ DEFAULT_DIGEST_RESULT_CHARS = 200;
25210
25283
  }
25211
25284
  });
25212
25285
 
@@ -25700,9 +25773,9 @@ function spillToolOutput(fullText, meta3) {
25700
25773
  const rnd = randomBytes(3).toString("hex");
25701
25774
  const safeTool = (meta3?.toolName ?? "tool").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
25702
25775
  const file2 = `${stamp}-${safeTool}-${hash3}-${rnd}.txt`;
25703
- const path43 = join11(dir, file2);
25704
- writeFileSync10(path43, fullText, "utf8");
25705
- return path43;
25776
+ const path44 = join11(dir, file2);
25777
+ writeFileSync10(path44, fullText, "utf8");
25778
+ return path44;
25706
25779
  } catch {
25707
25780
  return null;
25708
25781
  }
@@ -25748,10 +25821,10 @@ function truncateToolResult(text, capOrOpts = TOOL_RESULT_LINE_CAP) {
25748
25821
  ${tail}`;
25749
25822
  }
25750
25823
  if (doSpill) {
25751
- const path43 = spillToolOutput(text, { toolName: opts.toolName });
25752
- if (path43) {
25824
+ const path44 = spillToolOutput(text, { toolName: opts.toolName });
25825
+ if (path44) {
25753
25826
  const spillNote = `
25754
- \u2026 [full output spilled to: ${path43} \u2014 re-read with read_file if you need the complete text] \u2026`;
25827
+ \u2026 [full output spilled to: ${path44} \u2014 re-read with read_file if you need the complete text] \u2026`;
25755
25828
  if (preview.includes("] \u2026\n")) {
25756
25829
  preview = preview.replace("] \u2026\n", `] \u2026${spillNote}
25757
25830
  `);
@@ -26581,11 +26654,20 @@ function maxToolCallsForThoroughness(thoroughness, agent) {
26581
26654
  if (thoroughness === "deep") return 12;
26582
26655
  return 6;
26583
26656
  }
26584
- async function runSubAgent(harness) {
26657
+ async function runSubAgent(harness, opts = {}) {
26658
+ const { signal } = opts;
26585
26659
  let current = "";
26586
26660
  let lastCompleted = "";
26587
26661
  let error51;
26662
+ if (signal?.aborted) return { result: "", aborted: true };
26588
26663
  for await (const ev of harness.run()) {
26664
+ if (signal?.aborted) {
26665
+ return {
26666
+ result: (lastCompleted || current).trim(),
26667
+ ...error51 ? { error: error51 } : {},
26668
+ aborted: true
26669
+ };
26670
+ }
26589
26671
  switch (ev.type) {
26590
26672
  case "message_start":
26591
26673
  current = "";
@@ -26612,7 +26694,7 @@ async function runTentacle(opts) {
26612
26694
  const started = Date.now();
26613
26695
  const g = globalThis;
26614
26696
  let worktree = null;
26615
- let effectiveCwd = parentCwd;
26697
+ let effectiveCwd = opts.cwdOverride || parentCwd;
26616
26698
  const wantWt = agent === "general" && deps.allowWorktree !== false && isKrakenWorktreeEnabled();
26617
26699
  if (wantWt) {
26618
26700
  try {
@@ -26715,8 +26797,31 @@ async function runTentacle(opts) {
26715
26797
  error: `task: failed to start sub-agent \u2014 ${err instanceof Error ? err.message : String(err)}`
26716
26798
  };
26717
26799
  }
26718
- const { result, error: error51 } = await runSubAgent(harness);
26800
+ const { result, error: error51, aborted: aborted2 } = await runSubAgent(harness, {
26801
+ ...opts.signal ? { signal: opts.signal } : {}
26802
+ });
26719
26803
  const durationMs = Date.now() - started;
26804
+ if (aborted2) {
26805
+ if (worktree && !shouldKeepWorktree()) await cleanupKrakenWorktree(worktree);
26806
+ appendKrakenRadio(parentCwd, sessionId, {
26807
+ kind: "error",
26808
+ agent,
26809
+ thoroughness,
26810
+ description: args.description,
26811
+ detail: "cancelled: node timeout",
26812
+ model: sub.model,
26813
+ worktree: worktree?.path ?? null,
26814
+ durationMs,
26815
+ ok: false
26816
+ });
26817
+ krakenTentacleEnd(liveId, {
26818
+ ok: false,
26819
+ model: sub.model,
26820
+ detail: "cancelled",
26821
+ durationMs
26822
+ });
26823
+ return { ok: false, agent, error: "task: sub-agent cancelled (node timeout)", cancelled: true };
26824
+ }
26720
26825
  if (!result) {
26721
26826
  if (worktree) await cleanupKrakenWorktree(worktree);
26722
26827
  appendKrakenRadio(parentCwd, sessionId, {
@@ -28660,21 +28765,21 @@ function normalizeAuth(auth) {
28660
28765
  return "agent";
28661
28766
  }
28662
28767
  function readSecrets() {
28663
- const path43 = getSshSecretsPath();
28664
- if (!existsSync17(path43)) return {};
28768
+ const path44 = getSshSecretsPath();
28769
+ if (!existsSync17(path44)) return {};
28665
28770
  try {
28666
- return JSON.parse(readFileSync16(path43, "utf8"));
28771
+ return JSON.parse(readFileSync16(path44, "utf8"));
28667
28772
  } catch {
28668
28773
  return {};
28669
28774
  }
28670
28775
  }
28671
28776
  function writeSecrets(data) {
28672
- const path43 = getSshSecretsPath();
28673
- mkdirSync9(dirname2(path43), { recursive: true });
28674
- writeFileSync11(path43, `${JSON.stringify(data, null, 2)}
28777
+ const path44 = getSshSecretsPath();
28778
+ mkdirSync9(dirname2(path44), { recursive: true });
28779
+ writeFileSync11(path44, `${JSON.stringify(data, null, 2)}
28675
28780
  `, "utf8");
28676
28781
  try {
28677
- chmodSync(path43, 384);
28782
+ chmodSync(path44, 384);
28678
28783
  } catch {
28679
28784
  }
28680
28785
  }
@@ -28703,10 +28808,10 @@ function deleteSshPassword(id) {
28703
28808
  writeSecrets({ passwords });
28704
28809
  }
28705
28810
  function readStore2() {
28706
- const path43 = getSshTargetsPath();
28707
- if (!existsSync17(path43)) return [];
28811
+ const path44 = getSshTargetsPath();
28812
+ if (!existsSync17(path44)) return [];
28708
28813
  try {
28709
- const parsed = JSON.parse(readFileSync16(path43, "utf8"));
28814
+ const parsed = JSON.parse(readFileSync16(path44, "utf8"));
28710
28815
  const list = Array.isArray(parsed.targets) ? parsed.targets : [];
28711
28816
  return list.filter(
28712
28817
  (t) => t && typeof t.id === "string" && typeof t.host === "string" && typeof t.user === "string"
@@ -28721,11 +28826,11 @@ function readStore2() {
28721
28826
  }
28722
28827
  }
28723
28828
  function writeStore2(targets) {
28724
- const path43 = getSshTargetsPath();
28725
- mkdirSync9(dirname2(path43), { recursive: true });
28829
+ const path44 = getSshTargetsPath();
28830
+ mkdirSync9(dirname2(path44), { recursive: true });
28726
28831
  const clean = targets.map(({ hasPassword: _hp, ...t }) => t);
28727
28832
  writeFileSync11(
28728
- path43,
28833
+ path44,
28729
28834
  `${JSON.stringify({ targets: clean }, null, 2)}
28730
28835
  `,
28731
28836
  "utf8"
@@ -28971,11 +29076,11 @@ function formatSshTargetsForPrompt() {
28971
29076
  ];
28972
29077
  for (const t of targets) {
28973
29078
  const tags = t.tags?.length ? ` tags=[${t.tags.join(",")}]` : "";
28974
- const path43 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
29079
+ const path44 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
28975
29080
  const allow = t.allowedCommands?.length ? ` allowed=${t.allowedCommands.join("|")}` : " allowed=status-only";
28976
29081
  const auth = t.auth === "password" ? " auth=password" : t.auth === "keyPath" ? " auth=key" : " auth=agent";
28977
29082
  lines.push(
28978
- `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path43}${tags}${allow}`
29083
+ `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path44}${tags}${allow}`
28979
29084
  );
28980
29085
  }
28981
29086
  return lines.join("\n");
@@ -31827,28 +31932,28 @@ var init_storage = __esm({
31827
31932
  VALID_SCALARS = /^(true|false|null|~)$/i;
31828
31933
  Storage = class {
31829
31934
  /** Read a Markdown file with frontmatter. Throws if not found. */
31830
- read(path43) {
31831
- if (!existsSync25(path43)) {
31832
- throw new Error(`File not found: ${path43}`);
31935
+ read(path44) {
31936
+ if (!existsSync25(path44)) {
31937
+ throw new Error(`File not found: ${path44}`);
31833
31938
  }
31834
- const md = readFileSync21(path43, "utf8");
31939
+ const md = readFileSync21(path44, "utf8");
31835
31940
  return parseFrontmatter(md);
31836
31941
  }
31837
31942
  /** Read a Markdown file; returns null if not found. */
31838
- readIfExists(path43) {
31839
- if (!existsSync25(path43)) return null;
31840
- return this.read(path43);
31943
+ readIfExists(path44) {
31944
+ if (!existsSync25(path44)) return null;
31945
+ return this.read(path44);
31841
31946
  }
31842
31947
  /**
31843
31948
  * Write a Markdown file atomically (tmp + rename). Creates parent dirs.
31844
31949
  * The meta object is serialized as YAML frontmatter; body as Markdown.
31845
31950
  */
31846
- write(path43, meta3, body) {
31847
- mkdirSync11(dirname4(path43), { recursive: true });
31848
- const tmp = path43 + ".tmp-" + process.pid;
31951
+ write(path44, meta3, body) {
31952
+ mkdirSync11(dirname4(path44), { recursive: true });
31953
+ const tmp = path44 + ".tmp-" + process.pid;
31849
31954
  const md = serializeFrontmatter(meta3, body);
31850
31955
  writeFileSync13(tmp, md, "utf8");
31851
- renameSync2(tmp, path43);
31956
+ renameSync2(tmp, path44);
31852
31957
  }
31853
31958
  /** List all .md files in a directory (non-recursive). */
31854
31959
  listMarkdown(dir) {
@@ -31926,8 +32031,8 @@ function readPlan(ctx) {
31926
32031
  } catch {
31927
32032
  }
31928
32033
  }
31929
- const path43 = workspaceFile(ctx.rootDir, "plan");
31930
- const doc = ctx.storage.readIfExists(path43);
32034
+ const path44 = workspaceFile(ctx.rootDir, "plan");
32035
+ const doc = ctx.storage.readIfExists(path44);
31931
32036
  if (!doc) return { phases: [], tasks: [], milestones: [] };
31932
32037
  const meta3 = doc.meta;
31933
32038
  return {
@@ -32092,7 +32197,7 @@ function addMilestoneRecord(ctx, summary, input) {
32092
32197
  dueDate: input.dueDate,
32093
32198
  targetVersion: version2
32094
32199
  });
32095
- const path43 = join23(ctx.rootDir, "milestones", `${id}.md`);
32200
+ const path44 = join23(ctx.rootDir, "milestones", `${id}.md`);
32096
32201
  const meta3 = {
32097
32202
  kind: "milestone",
32098
32203
  id,
@@ -32109,7 +32214,7 @@ function addMilestoneRecord(ctx, summary, input) {
32109
32214
  `Target version: ${version2}`,
32110
32215
  ""
32111
32216
  ].join("\n");
32112
- ctx.storage.write(path43, meta3, body);
32217
+ ctx.storage.write(path44, meta3, body);
32113
32218
  return { id, created: true };
32114
32219
  }
32115
32220
  function readPlanSummary(ctx) {
@@ -32313,7 +32418,7 @@ function addIdeaStub(ctx) {
32313
32418
  const tags = args["tags"] ?? [];
32314
32419
  const category = args["category"] ?? "General";
32315
32420
  const id = `${nextAdrId(ctx)}-${slugify3(title)}`;
32316
- const path43 = workspaceArtifact(ctx.rootDir, "decisions", id);
32421
+ const path44 = workspaceArtifact(ctx.rootDir, "decisions", id);
32317
32422
  const meta3 = {
32318
32423
  kind: "adr",
32319
32424
  status: "proposed",
@@ -32339,7 +32444,7 @@ function addIdeaStub(ctx) {
32339
32444
  ...consequences.map((c) => `- ${c}`),
32340
32445
  ""
32341
32446
  ].join("\n");
32342
- ctx.storage.write(path43, meta3, body);
32447
+ ctx.storage.write(path44, meta3, body);
32343
32448
  return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
32344
32449
  });
32345
32450
  }
@@ -32421,14 +32526,14 @@ function createDocumentStub(ctx) {
32421
32526
  ctx.storage.write(risksPath, riskMeta, content);
32422
32527
  return `Document "${title}" created at risks.md (workspace root).`;
32423
32528
  }
32424
- const path43 = workspaceArtifact(ctx.rootDir, "docs", slug);
32529
+ const path44 = workspaceArtifact(ctx.rootDir, "docs", slug);
32425
32530
  const meta3 = {
32426
32531
  kind: "doc",
32427
32532
  id: slug,
32428
32533
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
32429
32534
  tags
32430
32535
  };
32431
- ctx.storage.write(path43, meta3, content);
32536
+ ctx.storage.write(path44, meta3, content);
32432
32537
  return `Document "${title}" created at docs/${slug}.md.`;
32433
32538
  });
32434
32539
  }
@@ -32972,10 +33077,10 @@ function getUserMcpPath() {
32972
33077
  function getProjectMcpPath(projectRoot) {
32973
33078
  return join24(projectRoot, ".zelari", "mcp.json");
32974
33079
  }
32975
- function readFile2(path43) {
32976
- if (!existsSync28(path43)) return {};
33080
+ function readFile2(path44) {
33081
+ if (!existsSync28(path44)) return {};
32977
33082
  try {
32978
- const parsed = JSON.parse(readFileSync23(path43, "utf8"));
33083
+ const parsed = JSON.parse(readFileSync23(path44, "utf8"));
32979
33084
  const out = {};
32980
33085
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
32981
33086
  if (!cfg || typeof cfg.command !== "string" || !cfg.command.trim()) continue;
@@ -32991,10 +33096,10 @@ function readFile2(path43) {
32991
33096
  return {};
32992
33097
  }
32993
33098
  }
32994
- function writeFile(path43, servers) {
32995
- mkdirSync13(dirname6(path43), { recursive: true });
33099
+ function writeFile(path44, servers) {
33100
+ mkdirSync13(dirname6(path44), { recursive: true });
32996
33101
  const body = { mcpServers: servers };
32997
- writeFileSync15(path43, `${JSON.stringify(body, null, 2)}
33102
+ writeFileSync15(path44, `${JSON.stringify(body, null, 2)}
32998
33103
  `, "utf8");
32999
33104
  }
33000
33105
  function listMcpServers(projectRoot) {
@@ -33027,9 +33132,9 @@ function upsertMcpServer(opts) {
33027
33132
  if (!opts.config.command?.trim()) {
33028
33133
  return { ok: false, error: "command is required" };
33029
33134
  }
33030
- let path43;
33135
+ let path44;
33031
33136
  if (opts.scope === "user") {
33032
- path43 = getUserMcpPath();
33137
+ path44 = getUserMcpPath();
33033
33138
  } else {
33034
33139
  const root = opts.projectRoot?.trim();
33035
33140
  if (!root) {
@@ -33038,30 +33143,30 @@ function upsertMcpServer(opts) {
33038
33143
  error: "projectRoot required for project scope (Open Folder first)"
33039
33144
  };
33040
33145
  }
33041
- path43 = getProjectMcpPath(root);
33146
+ path44 = getProjectMcpPath(root);
33042
33147
  }
33043
- const current = readFile2(path43);
33148
+ const current = readFile2(path44);
33044
33149
  current[name] = {
33045
33150
  command: opts.config.command.trim(),
33046
33151
  args: opts.config.args,
33047
33152
  env: opts.config.env,
33048
33153
  enabled: opts.config.enabled !== false
33049
33154
  };
33050
- writeFile(path43, current);
33051
- return { ok: true, path: path43 };
33155
+ writeFile(path44, current);
33156
+ return { ok: true, path: path44 };
33052
33157
  }
33053
33158
  function removeMcpServer(opts) {
33054
- const path43 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
33055
- if (!path43) {
33159
+ const path44 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
33160
+ if (!path44) {
33056
33161
  return { ok: false, error: "projectRoot required for project scope" };
33057
33162
  }
33058
- const current = readFile2(path43);
33163
+ const current = readFile2(path44);
33059
33164
  if (!(opts.name in current)) {
33060
- return { ok: false, error: `Server "${opts.name}" not found in ${path43}` };
33165
+ return { ok: false, error: `Server "${opts.name}" not found in ${path44}` };
33061
33166
  }
33062
33167
  delete current[opts.name];
33063
- writeFile(path43, current);
33064
- return { ok: true, path: path43 };
33168
+ writeFile(path44, current);
33169
+ return { ok: true, path: path44 };
33065
33170
  }
33066
33171
  var init_mcpConfigIo = __esm({
33067
33172
  "src/cli/mcp/mcpConfigIo.ts"() {
@@ -33398,10 +33503,10 @@ import { createHash as createHash5 } from "node:crypto";
33398
33503
  import { join as join26 } from "node:path";
33399
33504
  import { readFile as readFile3 } from "node:fs/promises";
33400
33505
  async function readPackageJson2(projectRoot) {
33401
- const path43 = join26(projectRoot, "package.json");
33402
- if (!existsSync30(path43)) return null;
33506
+ const path44 = join26(projectRoot, "package.json");
33507
+ if (!existsSync30(path44)) return null;
33403
33508
  try {
33404
- return JSON.parse(await readFile3(path43, "utf8"));
33509
+ return JSON.parse(await readFile3(path44, "utf8"));
33405
33510
  } catch {
33406
33511
  return null;
33407
33512
  }
@@ -33483,9 +33588,9 @@ async function genBuild(ctx) {
33483
33588
  ].join("\n");
33484
33589
  }
33485
33590
  async function genOpenQuestions(ctx) {
33486
- const path43 = join26(ctx.rootDir, "risks.md");
33487
- if (!existsSync30(path43)) return "_No open questions._";
33488
- const content = readFileSync25(path43, "utf8");
33591
+ const path44 = join26(ctx.rootDir, "risks.md");
33592
+ if (!existsSync30(path44)) return "_No open questions._";
33593
+ const content = readFileSync25(path44, "utf8");
33489
33594
  const lines = content.split("\n");
33490
33595
  const questions = [];
33491
33596
  let currentTitle = "";
@@ -34052,8 +34157,8 @@ async function runPostCouncilHook(ctx, options) {
34052
34157
  sources: scope.sources
34053
34158
  } : void 0
34054
34159
  });
34055
- const path43 = writeCouncilCompletion(ctx.rootDir, completion);
34056
- completionHook = { ran: true, path: path43, completion };
34160
+ const path44 = writeCouncilCompletion(ctx.rootDir, completion);
34161
+ completionHook = { ran: true, path: path44, completion };
34057
34162
  } catch (err) {
34058
34163
  completionHook = {
34059
34164
  ran: true,
@@ -35271,9 +35376,17 @@ __export(planner_exports, {
35271
35376
  KRAKEN_PLANNER_SYSTEM_PROMPT: () => KRAKEN_PLANNER_SYSTEM_PROMPT,
35272
35377
  PlannerTransportError: () => PlannerTransportError,
35273
35378
  buildGraphFromPlan: () => buildGraphFromPlan,
35379
+ buildPlannerUserPrompt: () => buildPlannerUserPrompt,
35274
35380
  extractJsonObject: () => extractJsonObject,
35275
- planTaskGraph: () => planTaskGraph
35381
+ planTaskGraph: () => planTaskGraph,
35382
+ stripReasoningBlocks: () => stripReasoningBlocks
35276
35383
  });
35384
+ function resolvePlannerWorkspaceChars(env = process.env) {
35385
+ const raw = env.ZELARI_KRAKEN_PLANNER_WORKSPACE_CHARS;
35386
+ if (raw === void 0 || raw === "") return DEFAULT_PLANNER_WORKSPACE_CHARS;
35387
+ const n = Number.parseInt(raw, 10);
35388
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_PLANNER_WORKSPACE_CHARS;
35389
+ }
35277
35390
  function resolvePlannerTimeoutMs(env = process.env) {
35278
35391
  const raw = env.ZELARI_KRAKEN_PLANNER_TIMEOUT_MS;
35279
35392
  if (raw === void 0 || raw === "") return DEFAULT_LLM_TIMEOUT_MS;
@@ -35286,22 +35399,77 @@ function resolvePlannerMaxTokens(env = process.env) {
35286
35399
  const n = Number.parseInt(raw, 10);
35287
35400
  return Number.isFinite(n) && n > 0 ? n : DEFAULT_LLM_MAX_TOKENS;
35288
35401
  }
35289
- function extractJsonObject(text) {
35290
- const trimmed = text.trim();
35291
- const fenced = /^```(?:json)?\s*([\s\S]*?)```$/i.exec(trimmed);
35292
- const body = fenced ? fenced[1].trim() : trimmed;
35293
- const balanced = extractBalancedJsonObject2(body);
35294
- const candidate = balanced ?? body;
35402
+ function extractJsonObject(text, opts = {}) {
35403
+ const stripped = stripReasoningBlocks(text);
35404
+ const usable = findJsonIn(stripped, opts);
35405
+ if (usable) return usable.value;
35406
+ const raw = findJsonIn(text, opts);
35407
+ if (raw) return raw.value;
35408
+ const body = bodyOf(stripped) || bodyOf(text);
35295
35409
  try {
35296
- return JSON.parse(candidate);
35410
+ return JSON.parse(body);
35297
35411
  } catch (err) {
35298
35412
  try {
35299
- return JSON.parse(repairLooseJson(candidate));
35413
+ return JSON.parse(repairLooseJson(body));
35300
35414
  } catch {
35301
35415
  throw err instanceof Error ? err : new Error(String(err));
35302
35416
  }
35303
35417
  }
35304
35418
  }
35419
+ function bodyOf(text) {
35420
+ const trimmed = text.trim();
35421
+ const fenced = /^```(?:json)?\s*([\s\S]*?)```$/i.exec(trimmed);
35422
+ return fenced ? fenced[1].trim() : trimmed;
35423
+ }
35424
+ function findJsonIn(text, opts) {
35425
+ let firstParsed = null;
35426
+ for (const candidate of balancedJsonObjects(bodyOf(text))) {
35427
+ let value;
35428
+ try {
35429
+ value = JSON.parse(candidate);
35430
+ } catch {
35431
+ try {
35432
+ value = JSON.parse(repairLooseJson(candidate));
35433
+ } catch {
35434
+ continue;
35435
+ }
35436
+ }
35437
+ if (!opts.requireKey) return { value };
35438
+ if (value && typeof value === "object" && opts.requireKey in value) {
35439
+ return { value };
35440
+ }
35441
+ const unwrapped = unwrapKey(value, opts.requireKey);
35442
+ if (unwrapped) return { value: unwrapped };
35443
+ if (!firstParsed) firstParsed = { value };
35444
+ }
35445
+ return firstParsed;
35446
+ }
35447
+ function unwrapKey(value, key) {
35448
+ if (!value || typeof value !== "object") return null;
35449
+ for (const child of Object.values(value)) {
35450
+ if (child && typeof child === "object" && key in child) {
35451
+ return child;
35452
+ }
35453
+ }
35454
+ return null;
35455
+ }
35456
+ function stripReasoningBlocks(text) {
35457
+ let out = text.replace(/<(think|thinking|reasoning)>[\s\S]*?<\/\1>/gi, "");
35458
+ const openIdx = out.search(/<(think|thinking|reasoning)>/i);
35459
+ if (openIdx >= 0) out = out.slice(0, openIdx);
35460
+ return out;
35461
+ }
35462
+ function* balancedJsonObjects(s) {
35463
+ let from = 0;
35464
+ while (from < s.length) {
35465
+ const next = extractBalancedJsonObject2(s.slice(from));
35466
+ if (!next) return;
35467
+ yield next;
35468
+ const idx = s.indexOf(next, from);
35469
+ if (idx < 0) return;
35470
+ from = idx + next.length;
35471
+ }
35472
+ }
35305
35473
  function extractBalancedJsonObject2(s) {
35306
35474
  const start = s.indexOf("{");
35307
35475
  if (start < 0) return null;
@@ -35527,11 +35695,32 @@ async function createDefaultLlmClient(opts) {
35527
35695
  }
35528
35696
  };
35529
35697
  }
35530
- function buildPlannerUserPrompt(prompt) {
35531
- return `Goal:
35532
- ${prompt.trim()}
35533
-
35534
- Return ONLY the JSON object described in the system prompt.`;
35698
+ function resolveWorkspaceListing(opts) {
35699
+ if (opts.workspace !== void 0) return opts.workspace || void 0;
35700
+ if (!opts.cwd) return void 0;
35701
+ const maxChars = resolvePlannerWorkspaceChars();
35702
+ if (maxChars <= 0) return void 0;
35703
+ try {
35704
+ return buildWorkspaceSummary(opts.cwd, { maxEntries: 24, maxChars }) || void 0;
35705
+ } catch {
35706
+ return void 0;
35707
+ }
35708
+ }
35709
+ function buildPlannerUserPrompt(prompt, opts = {}) {
35710
+ const parts = [`Goal:
35711
+ ${prompt.trim()}`];
35712
+ if (opts.workspace?.trim()) {
35713
+ parts.push(
35714
+ "",
35715
+ "## The project this goal is about (real files on disk)",
35716
+ opts.workspace.trim()
35717
+ );
35718
+ }
35719
+ if (opts.previousAttempt?.trim()) {
35720
+ parts.push("", opts.previousAttempt.trim());
35721
+ }
35722
+ parts.push("", "Return ONLY the JSON object described in the system prompt.");
35723
+ return parts.join("\n");
35535
35724
  }
35536
35725
  function uniqueId(base, existing) {
35537
35726
  if (!existing.has(base)) return base;
@@ -35540,11 +35729,40 @@ function uniqueId(base, existing) {
35540
35729
  return `${base}-${i}`;
35541
35730
  }
35542
35731
  function buildAutoVerifyPrompt(general) {
35543
- const acc = general.acceptance && general.acceptance.length > 0 ? `
35544
-
35545
- Acceptance criteria to check:
35546
- ${general.acceptance.map((a) => `- ${a}`).join("\n")}` : "";
35547
- return `Verify the following work was completed correctly on disk: ${general.label}.${acc}`;
35732
+ const taskPrompt = general.prompt.length > MAX_VERIFY_TASK_PROMPT_CHARS ? `${general.prompt.slice(0, MAX_VERIFY_TASK_PROMPT_CHARS)}
35733
+ \u2026 [truncated]` : general.prompt;
35734
+ const parts = [
35735
+ `Verify on disk that this work was actually completed correctly: ${general.label}.`,
35736
+ "",
35737
+ "## The task that was carried out",
35738
+ taskPrompt
35739
+ ];
35740
+ if (general.scope && general.scope.length > 0) {
35741
+ parts.push("", "## Paths the work was scoped to", ...general.scope.map((s) => `- ${s}`));
35742
+ }
35743
+ if (general.acceptance && general.acceptance.length > 0) {
35744
+ parts.push(
35745
+ "",
35746
+ "## Acceptance criteria to check explicitly",
35747
+ ...general.acceptance.map((a) => `- ${a}`)
35748
+ );
35749
+ }
35750
+ parts.push(
35751
+ "",
35752
+ "Read the files involved rather than trusting any summary. Report the commands you ran and every gap you found.",
35753
+ "",
35754
+ "## How to report your verdict",
35755
+ "End your final message with a line of exactly this form, as the LAST line:",
35756
+ "",
35757
+ "VERDICT: PASS",
35758
+ "",
35759
+ "or",
35760
+ "",
35761
+ "VERDICT: FAIL",
35762
+ "",
35763
+ "This line is parsed. FAIL sends the work back to the tentacle that wrote it, together with everything you write above the line \u2014 so state each gap concretely enough to be acted on (file, what is wrong, what it should be). Only report FAIL for a real defect against the task or its acceptance criteria: a rework round is expensive and there is only a small number of them. Stylistic preferences are not a FAIL."
35764
+ );
35765
+ return parts.join("\n");
35548
35766
  }
35549
35767
  function buildGraphFromPlan(graphId, planned) {
35550
35768
  const nodes = planned.map((p3) => ({
@@ -35597,14 +35815,23 @@ async function planTaskGraph(opts) {
35597
35815
  const client = opts.llmClient ?? await createDefaultLlmClient({ provider: opts.provider, model: opts.model });
35598
35816
  const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
35599
35817
  const graphId = opts.graphId ?? `kraken-${Date.now().toString(36)}`;
35600
- const userBase = buildPlannerUserPrompt(opts.prompt);
35818
+ const workspace = resolveWorkspaceListing(opts);
35819
+ const userBase = buildPlannerUserPrompt(opts.prompt, {
35820
+ ...opts.previousAttempt ? { previousAttempt: opts.previousAttempt } : {},
35821
+ ...workspace ? { workspace } : {}
35822
+ });
35601
35823
  let lastError;
35602
35824
  let userMessage = userBase;
35603
35825
  for (let attempt = 1; attempt <= MAX_PLAN_ATTEMPTS; attempt++) {
35604
35826
  try {
35605
35827
  const text = await client.complete({ system: KRAKEN_PLANNER_SYSTEM_PROMPT, user: userMessage });
35606
- const parsedJson = extractJsonObject(text);
35828
+ const parsedJson = extractJsonObject(text, { requireKey: "nodes" });
35607
35829
  const validated = PlannedGraphSchema.parse(parsedJson);
35830
+ if (!validated.nodes.some((n) => n.kind === "general")) {
35831
+ throw new Error(
35832
+ 'the plan contains no "general" node, so it cannot change anything \u2014 every node is read-only "explore". Include at least one "general" node that performs the actual work.'
35833
+ );
35834
+ }
35608
35835
  const graph = buildGraphFromPlan(graphId, validated.nodes);
35609
35836
  const check2 = validateGraph(graph, { maxNodes });
35610
35837
  if (!check2.ok) {
@@ -35623,7 +35850,7 @@ Your previous response was invalid (${lastError}). Return ONLY corrected JSON ma
35623
35850
  `kraken planner: failed to produce a valid task graph after ${MAX_PLAN_ATTEMPTS} attempts \u2014 ${lastError}`
35624
35851
  );
35625
35852
  }
35626
- var MAX_PLAN_ATTEMPTS, DEFAULT_LLM_TIMEOUT_MS, PlannerTransportError, DEFAULT_LLM_MAX_TOKENS, DEFAULT_MAX_RETRIES, KRAKEN_PLANNER_SYSTEM_PROMPT, PlannedNodeSchema, PlannedGraphSchema;
35853
+ var MAX_PLAN_ATTEMPTS, DEFAULT_PLANNER_WORKSPACE_CHARS, DEFAULT_LLM_TIMEOUT_MS, PlannerTransportError, DEFAULT_LLM_MAX_TOKENS, DEFAULT_MAX_RETRIES, KRAKEN_PLANNER_SYSTEM_PROMPT, PlannedNodeSchema, PlannedGraphSchema, MAX_VERIFY_TASK_PROMPT_CHARS;
35627
35854
  var init_planner = __esm({
35628
35855
  "src/cli/kraken/planner.ts"() {
35629
35856
  "use strict";
@@ -35632,7 +35859,9 @@ var init_planner = __esm({
35632
35859
  init_providerConfig();
35633
35860
  init_keyStore();
35634
35861
  init_openai_compatible();
35862
+ init_workspaceSummary();
35635
35863
  MAX_PLAN_ATTEMPTS = 2;
35864
+ DEFAULT_PLANNER_WORKSPACE_CHARS = 3e3;
35636
35865
  DEFAULT_LLM_TIMEOUT_MS = 3e5;
35637
35866
  PlannerTransportError = class extends Error {
35638
35867
  constructor(message) {
@@ -35660,12 +35889,14 @@ var init_planner = __esm({
35660
35889
  '- kind "general": can edit files for one bounded, self-contained unit of work.',
35661
35890
  '- Do NOT emit "verify", "fix", or "merge" nodes \u2014 the executor adds those automatically.',
35662
35891
  '- "id" must be short, unique, kebab-case (e.g. "e1", "g-auth", "g-ui").',
35663
- '- "prompt" must be fully self-contained: the sub-agent sees ONLY this prompt, not this conversation.',
35892
+ '- "prompt" must be self-contained: the sub-agent sees ONLY this prompt, not this conversation.',
35664
35893
  '- "deps" lists ids of nodes that must finish first (topological order); [] if none.',
35894
+ '- A node DOES receive what its "deps" reported when they finished \u2014 the executor injects their conclusions into its prompt. So write a dependent node as "using the findings above, \u2026" rather than duplicating the research its dependency will do.',
35665
35895
  '- When two "general" nodes touch disjoint parts of the codebase, give each a "scope" (path/glob allowlist) so they can run in parallel safely. If scopes might overlap, either omit scope (forces sequential execution) or add a dep between them.',
35896
+ '- The user message includes a listing of the real project on disk. Build every "scope" from paths that appear there (or new paths that clearly belong beside them) \u2014 an invented path makes the parallelism decision meaningless, since scopes are exactly what the executor uses to decide which writers may run at the same time.',
35666
35897
  '- Prefer one "explore" node feeding several parallel "general" nodes over one giant node.',
35667
35898
  "- Keep the graph small: most goals need 3-8 nodes total.",
35668
- '- "acceptance" (optional) lists concrete, checkable criteria for a "general" node.'
35899
+ '- "acceptance" (optional) lists concrete, checkable criteria for a "general" node. These are ENFORCED: the executor adds a verify tentacle that checks them on disk and can send the work back for a rework round when they are not met. Write criteria a reader can settle by opening a file or running a command ("exports slugify(input: string): string", "npm test passes"), never subjective ones ("the code is elegant") \u2014 a criterion nobody can check just burns a rework round.'
35669
35900
  ].join("\n");
35670
35901
  PlannedNodeSchema = external_exports.object({
35671
35902
  id: external_exports.string().min(1).max(64).regex(/^[a-zA-Z0-9_-]+$/, "id must be alphanumeric/dash/underscore"),
@@ -35679,6 +35910,118 @@ var init_planner = __esm({
35679
35910
  PlannedGraphSchema = external_exports.object({
35680
35911
  nodes: external_exports.array(PlannedNodeSchema).min(1).max(DEFAULT_MAX_NODES)
35681
35912
  });
35913
+ MAX_VERIFY_TASK_PROMPT_CHARS = 1200;
35914
+ }
35915
+ });
35916
+
35917
+ // src/cli/kraken/graphMemory.ts
35918
+ var graphMemory_exports = {};
35919
+ __export(graphMemory_exports, {
35920
+ formatSnapshotForPlanner: () => formatSnapshotForPlanner,
35921
+ loadGraphSnapshot: () => loadGraphSnapshot,
35922
+ saveGraphSnapshot: () => saveGraphSnapshot,
35923
+ toGraphSnapshot: () => toGraphSnapshot
35924
+ });
35925
+ import { promises as fs19 } from "node:fs";
35926
+ import path34 from "node:path";
35927
+ function snapshotPath(cwd) {
35928
+ return path34.join(cwd, SNAPSHOT_DIR, SNAPSHOT_FILE);
35929
+ }
35930
+ function toGraphSnapshot(graph, opts) {
35931
+ const unresolved = (opts.unresolvedFindings ?? []).map((u) => ({
35932
+ ...u,
35933
+ findings: u.findings.slice(0, MAX_SNAPSHOT_FINDINGS_CHARS)
35934
+ }));
35935
+ return {
35936
+ graphId: graph.id,
35937
+ goal: opts.goal,
35938
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
35939
+ converged: opts.converged,
35940
+ ...unresolved.length > 0 ? { unresolvedFindings: unresolved } : {},
35941
+ nodes: [...graph.nodes.values()].map((n) => ({
35942
+ id: n.id,
35943
+ kind: n.kind,
35944
+ label: n.label,
35945
+ status: n.status,
35946
+ ...n.scope && n.scope.length > 0 ? { scope: n.scope } : {},
35947
+ ...n.error ? { error: n.error.slice(0, 300) } : {}
35948
+ }))
35949
+ };
35950
+ }
35951
+ async function saveGraphSnapshot(cwd, snapshot) {
35952
+ try {
35953
+ await fs19.access(cwd);
35954
+ const file2 = snapshotPath(cwd);
35955
+ await fs19.mkdir(path34.dirname(file2), { recursive: true });
35956
+ await fs19.writeFile(file2, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
35957
+ } catch {
35958
+ }
35959
+ }
35960
+ async function loadGraphSnapshot(cwd) {
35961
+ try {
35962
+ const raw = await fs19.readFile(snapshotPath(cwd), "utf8");
35963
+ const parsed = JSON.parse(raw);
35964
+ if (!parsed || !Array.isArray(parsed.nodes)) return null;
35965
+ return parsed;
35966
+ } catch {
35967
+ return null;
35968
+ }
35969
+ }
35970
+ function formatSnapshotForPlanner(snapshot) {
35971
+ if (!snapshot) return "";
35972
+ const done = snapshot.nodes.filter((n) => n.status === "done");
35973
+ const failed = snapshot.nodes.filter((n) => n.status === "error");
35974
+ const skipped = snapshot.nodes.filter((n) => n.status === "skipped");
35975
+ const rejected = snapshot.unresolvedFindings ?? [];
35976
+ if (failed.length === 0 && skipped.length === 0 && rejected.length === 0) return "";
35977
+ const line = (n) => `- ${n.label}${n.scope ? ` [${n.scope.join(", ")}]` : ""}${n.error ? ` \u2014 ${n.error}` : ""}`;
35978
+ const rejectedIds = new Set(rejected.map((r) => r.nodeId));
35979
+ const outcome = [
35980
+ `${done.length} done`,
35981
+ `${failed.length} failed`,
35982
+ `${skipped.length} never ran`,
35983
+ ...rejected.length > 0 ? [`${rejected.length} rejected by review`] : []
35984
+ ].join(", ");
35985
+ const parts = [
35986
+ "",
35987
+ "## Previous unfinished task graph in this project",
35988
+ `Goal it was working on: "${snapshot.goal}"`,
35989
+ `It did NOT finish cleanly (${outcome}).`,
35990
+ "",
35991
+ "If that goal is unrelated to the one above, ignore this section entirely.",
35992
+ ""
35993
+ ];
35994
+ const cleanlyDone = done.filter((n) => !rejectedIds.has(n.id));
35995
+ if (cleanlyDone.length > 0) {
35996
+ parts.push("Already completed \u2014 do NOT redo this work:", ...cleanlyDone.map(line), "");
35997
+ }
35998
+ if (rejected.length > 0) {
35999
+ parts.push(
36000
+ "Completed but REJECTED by review \u2014 the code exists, the defects do not fix themselves:",
36001
+ ...rejected.map(
36002
+ (r) => `- ${r.label} \u2014 ${r.findings.trim().split("\n")[0] ?? "no detail"}`
36003
+ ),
36004
+ ""
36005
+ );
36006
+ }
36007
+ if (failed.length > 0) {
36008
+ parts.push("Failed \u2014 needs to be finished or repaired:", ...failed.map(line), "");
36009
+ }
36010
+ if (skipped.length > 0) {
36011
+ parts.push("Never ran (blocked by the failures above):", ...skipped.map(line), "");
36012
+ }
36013
+ parts.push(
36014
+ "Plan only the remaining work. Inspect what is already on disk before rewriting it, and prefer wiring/finishing existing modules over recreating them."
36015
+ );
36016
+ return parts.join("\n");
36017
+ }
36018
+ var SNAPSHOT_DIR, SNAPSHOT_FILE, MAX_SNAPSHOT_FINDINGS_CHARS;
36019
+ var init_graphMemory = __esm({
36020
+ "src/cli/kraken/graphMemory.ts"() {
36021
+ "use strict";
36022
+ SNAPSHOT_DIR = path34.join(".zelari", "kraken");
36023
+ SNAPSHOT_FILE = "last-graph.json";
36024
+ MAX_SNAPSHOT_FINDINGS_CHARS = 400;
35682
36025
  }
35683
36026
  });
35684
36027
 
@@ -35693,30 +36036,60 @@ var init_tentacle = __esm({
35693
36036
  // src/cli/kraken/executor.ts
35694
36037
  var executor_exports = {};
35695
36038
  __export(executor_exports, {
36039
+ DEFAULT_CANCEL_GRACE_MS: () => DEFAULT_CANCEL_GRACE_MS,
35696
36040
  DEFAULT_FIX_BUDGET: () => DEFAULT_FIX_BUDGET,
36041
+ DEFAULT_GRAPH_TIMEOUT_MS: () => DEFAULT_GRAPH_TIMEOUT_MS,
35697
36042
  DEFAULT_MAX_PARALLEL: () => DEFAULT_MAX_PARALLEL,
36043
+ DEFAULT_MAX_REVIEW_ROUNDS: () => DEFAULT_MAX_REVIEW_ROUNDS,
35698
36044
  DEFAULT_NODE_TIMEOUT_MS: () => DEFAULT_NODE_TIMEOUT_MS,
35699
36045
  DEFAULT_WRITER_NODE_TIMEOUT_MS: () => DEFAULT_WRITER_NODE_TIMEOUT_MS,
35700
36046
  KrakenGraphExecutor: () => KrakenGraphExecutor,
36047
+ MAX_UPSTREAM_CHARS_PER_DEP: () => MAX_UPSTREAM_CHARS_PER_DEP,
36048
+ MAX_UPSTREAM_CHARS_TOTAL: () => MAX_UPSTREAM_CHARS_TOTAL,
36049
+ buildUpstreamContext: () => buildUpstreamContext,
35701
36050
  isKrakenGraphEnabled: () => isKrakenGraphEnabled,
35702
36051
  isWorldModelGateEnabled: () => isWorldModelGateEnabled,
36052
+ resolveCancelGraceMs: () => resolveCancelGraceMs,
35703
36053
  resolveFixBudget: () => resolveFixBudget,
36054
+ resolveGraphTimeoutMs: () => resolveGraphTimeoutMs,
35704
36055
  resolveMaxParallel: () => resolveMaxParallel,
35705
- resolveNodeTimeoutMs: () => resolveNodeTimeoutMs
36056
+ resolveMaxReviewRounds: () => resolveMaxReviewRounds,
36057
+ resolveNodeTimeoutMs: () => resolveNodeTimeoutMs,
36058
+ thoroughnessForKind: () => thoroughnessForKind
35706
36059
  });
35707
36060
  import { existsSync as existsSync34 } from "node:fs";
35708
- import path34 from "node:path";
36061
+ import path35 from "node:path";
35709
36062
  function resolveMaxParallel(env = process.env) {
35710
36063
  const raw = env.ZELARI_KRAKEN_MAX_PARALLEL;
35711
36064
  if (raw === void 0 || raw === "") return DEFAULT_MAX_PARALLEL;
35712
36065
  const n = Number.parseInt(raw, 10);
35713
36066
  return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_PARALLEL;
35714
36067
  }
35715
- function resolveFixBudget(env = process.env) {
36068
+ function resolveFixBudget(env = process.env, nodeCount = 0) {
35716
36069
  const raw = env.ZELARI_KRAKEN_FIX_BUDGET;
35717
- if (raw === void 0 || raw === "") return DEFAULT_FIX_BUDGET;
36070
+ if (raw === void 0 || raw === "") {
36071
+ return Math.max(DEFAULT_FIX_BUDGET, Math.ceil(nodeCount / 2));
36072
+ }
35718
36073
  const n = Number.parseInt(raw, 10);
35719
- return Number.isFinite(n) && n >= 0 ? n : DEFAULT_FIX_BUDGET;
36074
+ return Number.isFinite(n) && n >= 0 ? n : Math.max(DEFAULT_FIX_BUDGET, Math.ceil(nodeCount / 2));
36075
+ }
36076
+ function resolveMaxReviewRounds(env = process.env) {
36077
+ const raw = env.ZELARI_KRAKEN_MAX_REVIEW_ROUNDS;
36078
+ if (raw === void 0 || raw === "") return DEFAULT_MAX_REVIEW_ROUNDS;
36079
+ const n = Number.parseInt(raw, 10);
36080
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MAX_REVIEW_ROUNDS;
36081
+ }
36082
+ function resolveGraphTimeoutMs(env = process.env) {
36083
+ const raw = env.ZELARI_KRAKEN_GRAPH_TIMEOUT_MS;
36084
+ if (raw === void 0 || raw === "") return DEFAULT_GRAPH_TIMEOUT_MS;
36085
+ const n = Number.parseInt(raw, 10);
36086
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_GRAPH_TIMEOUT_MS;
36087
+ }
36088
+ function resolveCancelGraceMs(env = process.env) {
36089
+ const raw = env.ZELARI_KRAKEN_CANCEL_GRACE_MS;
36090
+ if (raw === void 0 || raw === "") return DEFAULT_CANCEL_GRACE_MS;
36091
+ const n = Number.parseInt(raw, 10);
36092
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_CANCEL_GRACE_MS;
35720
36093
  }
35721
36094
  function resolveNodeTimeoutMs(env = process.env, agent) {
35722
36095
  const raw = env.ZELARI_KRAKEN_NODE_TIMEOUT_MS;
@@ -35743,12 +36116,56 @@ function isWorldModelGateEnabled(cwd, env = process.env, checksExists = defaultC
35743
36116
  }
35744
36117
  function defaultChecksExists(cwd) {
35745
36118
  try {
35746
- return existsSync34(path34.join(cwd, ".zelari", "world", "checks.json"));
36119
+ return existsSync34(path35.join(cwd, ".zelari", "world", "checks.json"));
35747
36120
  } catch {
35748
36121
  return false;
35749
36122
  }
35750
36123
  }
35751
- var DEFAULT_MAX_PARALLEL, DEFAULT_FIX_BUDGET, DEFAULT_NODE_TIMEOUT_MS, DEFAULT_WRITER_NODE_TIMEOUT_MS, KrakenGraphExecutor;
36124
+ function buildUpstreamContext(graph, node) {
36125
+ const parts = [];
36126
+ const omitted = [];
36127
+ let budget = MAX_UPSTREAM_CHARS_TOTAL;
36128
+ for (const depId of node.deps) {
36129
+ const dep = graph.nodes.get(depId);
36130
+ if (!dep || dep.status !== "done") continue;
36131
+ const raw = (dep.result ?? "").trim();
36132
+ if (!raw) continue;
36133
+ const cap2 = Math.min(MAX_UPSTREAM_CHARS_PER_DEP, budget);
36134
+ if (cap2 <= 0) {
36135
+ omitted.push(dep.label);
36136
+ continue;
36137
+ }
36138
+ const body = raw.length > cap2 ? `${raw.slice(0, cap2)}
36139
+ \u2026 [truncated ${raw.length}\u2192${cap2} chars]` : raw;
36140
+ budget -= Math.min(raw.length, cap2);
36141
+ const scope = dep.scope && dep.scope.length > 0 ? `, scope: ${dep.scope.join(", ")}` : "";
36142
+ parts.push(`### ${dep.label} (${dep.kind}${scope})
36143
+ ${body}`);
36144
+ }
36145
+ if (parts.length === 0) return "";
36146
+ const lines = [
36147
+ "",
36148
+ "## Context from completed upstream tasks",
36149
+ "Results reported by the tasks this one depends on. Treat them as hypotheses \u2014 prefer the actual files on disk where they conflict.",
36150
+ "",
36151
+ ...parts
36152
+ ];
36153
+ if (omitted.length > 0) {
36154
+ lines.push("", `(omitted for context budget: ${omitted.join(", ")})`);
36155
+ }
36156
+ return lines.join("\n");
36157
+ }
36158
+ function thoroughnessForKind(kind) {
36159
+ return kind === "general" || kind === "fix" ? "deep" : "medium";
36160
+ }
36161
+ function firstLine(text, maxChars = 160) {
36162
+ const line = text.trim().split("\n").find((l) => l.trim() !== "")?.trim() ?? "";
36163
+ return line.length > maxChars ? `${line.slice(0, maxChars)}\u2026` : line;
36164
+ }
36165
+ function agentForNode(node) {
36166
+ return node.kind === "explore" || node.kind === "verify" ? node.kind : "general";
36167
+ }
36168
+ var DEFAULT_MAX_PARALLEL, DEFAULT_FIX_BUDGET, DEFAULT_MAX_REVIEW_ROUNDS, DEFAULT_GRAPH_TIMEOUT_MS, DEFAULT_NODE_TIMEOUT_MS, DEFAULT_WRITER_NODE_TIMEOUT_MS, DEFAULT_CANCEL_GRACE_MS, MAX_UPSTREAM_CHARS_PER_DEP, MAX_UPSTREAM_CHARS_TOTAL, KrakenGraphExecutor;
35752
36169
  var init_executor = __esm({
35753
36170
  "src/cli/kraken/executor.ts"() {
35754
36171
  "use strict";
@@ -35758,40 +36175,116 @@ var init_executor = __esm({
35758
36175
  init_krakenRadio();
35759
36176
  init_worldModel();
35760
36177
  init_graphStatus();
36178
+ init_graphMemory();
35761
36179
  DEFAULT_MAX_PARALLEL = 12;
35762
36180
  DEFAULT_FIX_BUDGET = 3;
36181
+ DEFAULT_MAX_REVIEW_ROUNDS = 1;
36182
+ DEFAULT_GRAPH_TIMEOUT_MS = 0;
35763
36183
  DEFAULT_NODE_TIMEOUT_MS = 3e5;
35764
36184
  DEFAULT_WRITER_NODE_TIMEOUT_MS = 9e5;
36185
+ DEFAULT_CANCEL_GRACE_MS = 3e4;
36186
+ MAX_UPSTREAM_CHARS_PER_DEP = 2800;
36187
+ MAX_UPSTREAM_CHARS_TOTAL = 8e3;
35765
36188
  KrakenGraphExecutor = class {
35766
36189
  deps;
35767
36190
  parentCwd;
35768
36191
  sessionId;
36192
+ goal;
35769
36193
  maxParallel;
35770
36194
  /** Explicit all-kinds override; when undefined the budget is per-kind. */
35771
36195
  nodeTimeoutMs;
36196
+ cancelGraceMs;
36197
+ /** Explicit override; when undefined the budget scales with the graph size. */
36198
+ fixBudgetOption;
35772
36199
  fixBudgetRemaining;
36200
+ maxReviewRounds;
36201
+ graphTimeoutMs;
35773
36202
  worldModelGateOverride;
35774
36203
  runTentacleFn;
35775
36204
  mergeFn;
35776
36205
  backtestFn;
35777
36206
  nodeRunState = /* @__PURE__ */ new Map();
36207
+ /** fix node id → id of the failed node it was spawned to repair. */
36208
+ repairs = /* @__PURE__ */ new Map();
36209
+ /**
36210
+ * rework node id → id of the writer whose work it is redoing. A rework is a
36211
+ * `fix` node, but unlike a repair it must NOT create a worktree of its own:
36212
+ * it edits the writer's existing one (see {@link spawnReworkPair}).
36213
+ */
36214
+ reworks = /* @__PURE__ */ new Map();
36215
+ /** lineage root writer id → rework rounds already spent on that lineage. */
36216
+ reviewRounds = /* @__PURE__ */ new Map();
36217
+ /**
36218
+ * rework node id → the ORIGINAL writer its lineage started from.
36219
+ *
36220
+ * The budget has to be per lineage, not per node: a rework is itself a
36221
+ * writer, so counting rounds against the node being reworked reset the
36222
+ * counter every round and the graph chained rework → verify → rework
36223
+ * forever, terminating only on the scheduler's iteration cap.
36224
+ */
36225
+ reviewLineage = /* @__PURE__ */ new Map();
36226
+ /** Verify verdicts left unresolved when the run ends. */
36227
+ unresolved = [];
36228
+ /** Live cancellation handles for the tentacles currently running. */
36229
+ nodeControllers = /* @__PURE__ */ new Map();
36230
+ /** Wall-clock duration of each node's last run, by node id. */
36231
+ durationsMs = /* @__PURE__ */ new Map();
36232
+ signal;
36233
+ /** Set once the run has been cancelled: stops admission, retries and fixes. */
36234
+ aborted = false;
35778
36235
  fixCounter = 0;
35779
36236
  constructor(opts) {
35780
36237
  this.deps = opts.taskToolDeps;
35781
36238
  this.parentCwd = opts.parentCwd;
35782
36239
  this.sessionId = opts.sessionId;
36240
+ this.goal = opts.goal;
35783
36241
  this.maxParallel = opts.maxParallel ?? resolveMaxParallel();
35784
36242
  this.nodeTimeoutMs = opts.nodeTimeoutMs;
36243
+ this.cancelGraceMs = opts.cancelGraceMs;
36244
+ this.fixBudgetOption = opts.fixBudget;
35785
36245
  this.fixBudgetRemaining = opts.fixBudget ?? resolveFixBudget();
36246
+ this.maxReviewRounds = opts.maxReviewRounds ?? resolveMaxReviewRounds();
36247
+ this.graphTimeoutMs = opts.graphTimeoutMs ?? resolveGraphTimeoutMs();
35786
36248
  this.worldModelGateOverride = opts.worldModelGate;
36249
+ this.signal = opts.signal;
35787
36250
  this.runTentacleFn = opts.runTentacleFn ?? runTentacle;
35788
36251
  this.mergeFn = opts.mergeFn ?? mergeKrakenWorktree;
35789
36252
  this.backtestFn = opts.backtestFn ?? runBacktest;
35790
36253
  }
35791
36254
  /** Execute the graph in place (mutates node statuses) until it settles. */
35792
36255
  async execute(graph) {
36256
+ if (this.fixBudgetOption === void 0) {
36257
+ this.fixBudgetRemaining = resolveFixBudget(process.env, graph.nodes.size);
36258
+ }
36259
+ const onAbort = () => this.cancelRun();
36260
+ if (this.signal) {
36261
+ if (this.signal.aborted) this.aborted = true;
36262
+ else this.signal.addEventListener("abort", onAbort, { once: true });
36263
+ }
36264
+ let graphTimer;
36265
+ if (this.graphTimeoutMs > 0) {
36266
+ graphTimer = setTimeout(() => {
36267
+ this.radio("graph_failed", {
36268
+ description: "graph executor",
36269
+ detail: `graph exceeded its ${this.graphTimeoutMs}ms wall-clock budget \u2014 cancelling`,
36270
+ ok: false
36271
+ });
36272
+ this.cancelRun();
36273
+ }, this.graphTimeoutMs);
36274
+ graphTimer.unref?.();
36275
+ }
36276
+ try {
36277
+ return await this.schedule(graph);
36278
+ } finally {
36279
+ if (graphTimer) clearTimeout(graphTimer);
36280
+ this.signal?.removeEventListener("abort", onAbort);
36281
+ }
36282
+ }
36283
+ /** The scheduling loop proper. See {@link execute} for the cancellation wrapper. */
36284
+ async schedule(graph) {
35793
36285
  const maxIterations = Math.max(64, graph.nodes.size * 8);
35794
36286
  let iterations = 0;
36287
+ const inFlight = /* @__PURE__ */ new Map();
35795
36288
  startKrakenGraphLive(graph);
35796
36289
  while (!isSettled(graph)) {
35797
36290
  iterations += 1;
@@ -35803,24 +36296,39 @@ var init_executor = __esm({
35803
36296
  });
35804
36297
  break;
35805
36298
  }
35806
- const ready = getReadyNodes(graph);
35807
- if (ready.length === 0) {
35808
- const skippedAny = this.skipBlockedNodes(graph);
35809
- if (!skippedAny) {
36299
+ if (this.aborted && inFlight.size === 0) break;
36300
+ const admitted = this.aborted ? [] : this.admit(graph, inFlight);
36301
+ if (admitted.length > 0) {
36302
+ for (const node of admitted) node.status = "running";
36303
+ updateKrakenGraphLive(graph);
36304
+ for (const node of admitted) inFlight.set(node.id, this.runNodeSafely(node, graph));
36305
+ }
36306
+ if (inFlight.size === 0) {
36307
+ if (!this.skipBlockedNodes(graph)) {
35810
36308
  break;
35811
36309
  }
35812
36310
  continue;
35813
36311
  }
35814
- const wave = selectParallelWave(ready).slice(0, this.maxParallel);
35815
- for (const node of wave) node.status = "running";
35816
- const results = await Promise.all(wave.map((node) => this.runNode(node, graph)));
35817
- for (let i = 0; i < wave.length; i++) {
35818
- this.applyResult(graph, wave[i], results[i]);
35819
- }
36312
+ const { id, res } = await Promise.race(inFlight.values());
36313
+ inFlight.delete(id);
36314
+ const settledNode = graph.nodes.get(id);
36315
+ if (settledNode) this.applyResult(graph, settledNode, res);
35820
36316
  updateKrakenGraphLive(graph);
35821
36317
  }
36318
+ if (inFlight.size > 0) {
36319
+ for (const { id, res } of await Promise.all(inFlight.values())) {
36320
+ const node = graph.nodes.get(id);
36321
+ if (node) this.applyResult(graph, node, res);
36322
+ }
36323
+ inFlight.clear();
36324
+ }
36325
+ if (this.aborted) {
36326
+ for (const n of graph.nodes.values()) {
36327
+ if (n.status === "pending") n.status = "skipped";
36328
+ }
36329
+ }
35822
36330
  let backtest;
35823
- const converged = isConverged(graph);
36331
+ const converged = !this.aborted && isConverged(graph);
35824
36332
  if (converged) {
35825
36333
  const gateOn = this.worldModelGateOverride ?? isWorldModelGateEnabled(this.parentCwd);
35826
36334
  if (gateOn) {
@@ -35834,47 +36342,140 @@ var init_executor = __esm({
35834
36342
  } else {
35835
36343
  this.radio("graph_failed", {
35836
36344
  description: "graph executor",
35837
- detail: `failed nodes: ${failedNodeIds(graph).join(", ") || "none"}`,
36345
+ detail: this.aborted ? "cancelled by caller" : `failed nodes: ${failedNodeIds(graph).join(", ") || "none"}`,
35838
36346
  ok: false
35839
36347
  });
35840
36348
  }
35841
36349
  endKrakenGraphLive(graph, converged);
36350
+ await saveGraphSnapshot(
36351
+ this.parentCwd,
36352
+ toGraphSnapshot(graph, {
36353
+ goal: this.goal ?? graph.id,
36354
+ converged,
36355
+ unresolvedFindings: this.unresolved
36356
+ })
36357
+ );
35842
36358
  return {
35843
36359
  graph,
35844
36360
  converged,
35845
36361
  failedNodeIds: failedNodeIds(graph),
35846
36362
  counts: countByStatus(graph),
36363
+ durationsMs: Object.fromEntries(this.durationsMs),
36364
+ cancelled: this.aborted,
36365
+ unresolvedFindings: [...this.unresolved],
35847
36366
  ...backtest ? { backtest } : {}
35848
36367
  };
35849
36368
  }
36369
+ /**
36370
+ * Stop the run: no further admissions, and every tentacle currently running
36371
+ * is told to unwind. Each node's own timeout/grace machinery then resolves
36372
+ * it, so `execute()` settles instead of leaving orphans behind.
36373
+ */
36374
+ cancelRun() {
36375
+ if (this.aborted) return;
36376
+ this.aborted = true;
36377
+ this.radio("graph_failed", {
36378
+ description: "graph executor",
36379
+ detail: `cancelling ${this.nodeControllers.size} running tentacle(s)`,
36380
+ ok: false
36381
+ });
36382
+ for (const controller of this.nodeControllers.values()) controller.abort();
36383
+ }
36384
+ /**
36385
+ * Pick the ready nodes that may start right now: parallel-safe against every
36386
+ * node already running AND against each other, within the concurrency cap.
36387
+ *
36388
+ * Unlike a wave-at-a-time scheduler this is called on every completion, so a
36389
+ * node becomes eligible the moment its blocker settles instead of waiting
36390
+ * for the slowest member of some earlier batch.
36391
+ */
36392
+ admit(graph, inFlight) {
36393
+ const capacity = this.maxParallel - inFlight.size;
36394
+ if (capacity <= 0) return [];
36395
+ const running = [];
36396
+ for (const id of inFlight.keys()) {
36397
+ const n = graph.nodes.get(id);
36398
+ if (n) running.push(n);
36399
+ }
36400
+ const admitted = [];
36401
+ for (const node of getReadyNodes(graph)) {
36402
+ if (admitted.length >= capacity) break;
36403
+ const safe = running.every((r) => canRunParallel(r, node)) && admitted.every((a) => canRunParallel(a, node));
36404
+ if (safe) admitted.push(node);
36405
+ }
36406
+ return admitted;
36407
+ }
36408
+ /**
36409
+ * Run one node, tagging the result with its id and converting an unexpected
36410
+ * throw into a node failure. The scheduler races these promises, so a
36411
+ * rejection would abandon every other in-flight tentacle mid-write; one
36412
+ * failed node that the retry/fix machinery can reason about is strictly
36413
+ * better than an aborted graph.
36414
+ */
36415
+ runNodeSafely(node, graph) {
36416
+ return this.runNode(node, graph).then(
36417
+ (res) => ({ id: node.id, res }),
36418
+ (err) => ({
36419
+ id: node.id,
36420
+ res: {
36421
+ ok: false,
36422
+ agent: agentForNode(node),
36423
+ error: `tentacle threw: ${err instanceof Error ? err.message : String(err)}`,
36424
+ cancelled: true
36425
+ }
36426
+ })
36427
+ );
36428
+ }
35850
36429
  /** Run one node: dispatch to the merge handler for `merge` nodes, else a tentacle. */
35851
36430
  async runNode(node, graph) {
36431
+ const startedAt = Date.now();
36432
+ try {
36433
+ return await this.runNodeInner(node, graph);
36434
+ } finally {
36435
+ this.durationsMs.set(node.id, Date.now() - startedAt);
36436
+ this.nodeControllers.delete(node.id);
36437
+ }
36438
+ }
36439
+ async runNodeInner(node, graph) {
35852
36440
  this.radio("node_start", { description: node.label, agent: node.kind });
35853
36441
  if (node.kind === "merge") {
35854
36442
  return this.runMergeNode(node, graph);
35855
36443
  }
35856
- const usesWorktree = node.kind === "general" || node.kind === "fix";
36444
+ const isRework = this.reworks.has(node.id);
36445
+ const usesWorktree = (node.kind === "general" || node.kind === "fix") && !isRework;
35857
36446
  const agent = node.kind === "fix" ? "general" : node.kind;
36447
+ const controller = new AbortController();
36448
+ this.nodeControllers.set(node.id, controller);
36449
+ if (this.aborted) controller.abort();
36450
+ const upstream = buildUpstreamContext(graph, node);
36451
+ const inheritedCwd = node.kind === "verify" || isRework ? this.inheritedWorktreeCwdFor(node, graph) : void 0;
35858
36452
  const res = await this.withNodeTimeout(
35859
36453
  this.runTentacleFn({
35860
- deps: this.deps,
36454
+ // `allowWorktree: false` is what actually stops a rework from opening
36455
+ // its own worktree: creation is driven by the agent kind ('general')
36456
+ // inside runTentacle, not by anything the executor passes per-call.
36457
+ deps: isRework ? { ...this.deps, allowWorktree: false } : this.deps,
35861
36458
  args: {
35862
36459
  description: node.label,
35863
- prompt: node.prompt,
36460
+ prompt: upstream ? `${node.prompt}
36461
+ ${upstream}` : node.prompt,
35864
36462
  scope: node.scope,
35865
36463
  acceptance: node.acceptance
35866
36464
  },
35867
36465
  agent,
35868
- thoroughness: "medium",
36466
+ thoroughness: thoroughnessForKind(node.kind),
35869
36467
  parentCwd: this.parentCwd,
36468
+ ...inheritedCwd ? { cwdOverride: inheritedCwd } : {},
35870
36469
  sessionId: this.sessionId,
35871
36470
  // Defer merge for writers so the executor controls merge ordering
35872
- // (Correction 4); explore/verify never create a worktree.
36471
+ // (Correction 4); explore/verify/rework never create a worktree.
35873
36472
  deferMerge: usesWorktree,
35874
36473
  graphId: graph.id,
35875
- nodeId: node.id
36474
+ nodeId: node.id,
36475
+ signal: controller.signal
35876
36476
  }),
35877
- agent
36477
+ agent,
36478
+ controller
35878
36479
  );
35879
36480
  if (res.ok && usesWorktree) {
35880
36481
  this.nodeRunState.set(node.id, { worktreeHandle: res.worktreeHandle });
@@ -35883,41 +36484,116 @@ var init_executor = __esm({
35883
36484
  }
35884
36485
  /**
35885
36486
  * Bound a tentacle run to its wall-clock budget (explicit `nodeTimeoutMs`
35886
- * option, else per-kind — writers get more than readers). On timeout, resolves
35887
- * to a synthetic `TentacleFailure` so the normal retry/fix/cascade-skip
35888
- * path handles it this only bounds how long the EXECUTOR waits, it does
35889
- * not (cannot) forcibly cancel the underlying sub-agent run; the point is
35890
- * to guarantee `execute()` itself always settles so the caller's process
35891
- * can exit instead of hanging forever on one stuck node.
36487
+ * option, else per-kind — writers get more than readers). On timeout the
36488
+ * run is CANCELLED via its AbortSignal, then given `cancelGraceMs` to
36489
+ * unwind. Cancellation lands at the sub-agent's next event boundary (an
36490
+ * async generator only observes `.return()` when it next yields), so a run
36491
+ * blocked on a slow provider call can outlive the grace period — that case
36492
+ * resolves with `cancelled: false` and `applyResult` refuses to re-spawn
36493
+ * the node, since a tentacle that may still be writing must not be joined
36494
+ * by a second one on the same scope.
36495
+ *
36496
+ * Either way `execute()` always settles, so the caller's process can exit
36497
+ * instead of hanging forever on one stuck node.
35892
36498
  */
35893
- withNodeTimeout(promise2, agent) {
36499
+ async withNodeTimeout(promise2, agent, controller) {
35894
36500
  const ms = this.nodeTimeoutMs ?? resolveNodeTimeoutMs(process.env, agent);
35895
36501
  if (ms <= 0) return promise2;
35896
36502
  let timer;
36503
+ const TIMED_OUT = Symbol("timeout");
35897
36504
  const timeout = new Promise((resolve3) => {
35898
- timer = setTimeout(() => {
35899
- resolve3({ ok: false, agent, error: `tentacle timed out after ${ms}ms` });
35900
- }, ms);
36505
+ timer = setTimeout(() => resolve3(TIMED_OUT), ms);
35901
36506
  });
35902
- return Promise.race([promise2, timeout]).finally(() => {
36507
+ const raced = await Promise.race([promise2, timeout]).finally(() => {
35903
36508
  if (timer) clearTimeout(timer);
35904
36509
  });
36510
+ if (raced !== TIMED_OUT) return raced;
36511
+ controller?.abort();
36512
+ const graceMs = this.cancelGraceMs ?? resolveCancelGraceMs();
36513
+ let graceTimer;
36514
+ const grace = new Promise((resolve3) => {
36515
+ graceTimer = setTimeout(() => resolve3(TIMED_OUT), graceMs);
36516
+ });
36517
+ const settled = await Promise.race([promise2, grace]).finally(() => {
36518
+ if (graceTimer) clearTimeout(graceTimer);
36519
+ });
36520
+ if (settled !== TIMED_OUT) {
36521
+ if (settled.ok) return settled;
36522
+ return { ...settled, cancelled: true };
36523
+ }
36524
+ return {
36525
+ ok: false,
36526
+ agent,
36527
+ error: `tentacle timed out after ${ms}ms and did not stop within ${graceMs}ms of being cancelled; not retrying to avoid two tentacles writing the same scope`,
36528
+ cancelled: false
36529
+ };
35905
36530
  }
35906
36531
  /**
35907
- * Sequentially merge every dep node's deferred worktree (in dep order) into
35908
- * parent HEAD. Deps without a recorded worktree handle (worktree isolation
35909
- * disabled, or a read-only node) are a no-op. On conflict the branch is
35910
- * kept and the conflict is surfaced in the merge node's error remaining
35911
- * deps still attempt to merge (independent branches shouldn't be blocked
35912
- * by one conflict).
36532
+ * The worktree a node should run in, inherited from the writer behind it.
36533
+ *
36534
+ * For a `verify`: verification happens BEFORE the merge node, so when its
36535
+ * writer worked in an isolated worktree the changes are not in the parent
36536
+ * tree yet a verify tentacle pointed at `parentCwd` was inspecting a tree
36537
+ * that provably did not contain the work it was asked to check, and reported
36538
+ * it missing.
36539
+ *
36540
+ * For a rework: the same tree, for the stronger reason that writing anywhere
36541
+ * else would strand the round on a second branch.
36542
+ *
36543
+ * Returns undefined when there is no single tree: no worktrees (isolation
36544
+ * disabled — the writers edited the parent tree directly), or several
36545
+ * distinct ones, in which case no single cwd is correct and the parent tree
36546
+ * is the honest default.
36547
+ */
36548
+ inheritedWorktreeCwdFor(node, graph) {
36549
+ const paths = new Set(
36550
+ this.collectWorktreeSources(node, graph).map((s) => s.handle.path)
36551
+ );
36552
+ return paths.size === 1 ? [...paths][0] : void 0;
36553
+ }
36554
+ /**
36555
+ * Resolve the deferred worktrees produced behind a node's dependencies, in
36556
+ * ancestors-first order. Used to decide what a `merge` node must merge, and
36557
+ * which tree a `verify` node should actually inspect.
36558
+ *
36559
+ * A merge node's direct deps are NOT the writers: `buildGraphFromPlan`
36560
+ * injects a `verify` node after every `general` node and points the merge at
36561
+ * those verifies, while worktree handles are recorded against the writer
36562
+ * node ids. Looking only at direct deps therefore found nothing to merge and
36563
+ * silently reported success while every tentacle's work stayed stranded on
36564
+ * its branch. Walk up through non-writer deps until the writers are found.
36565
+ *
36566
+ * Post-order so a writer that depends on another writer merges after it (the
36567
+ * later branch was cut from a HEAD that already contained the earlier work).
36568
+ * `merge` nodes terminate the walk: another merge already owns its subtree.
36569
+ */
36570
+ collectWorktreeSources(node, graph) {
36571
+ const out = [];
36572
+ const seen = /* @__PURE__ */ new Set();
36573
+ const visit = (id) => {
36574
+ if (seen.has(id)) return;
36575
+ seen.add(id);
36576
+ const n = graph.nodes.get(id);
36577
+ if (!n || n.kind === "merge") return;
36578
+ for (const dep of n.deps) visit(dep);
36579
+ const handle = this.nodeRunState.get(id)?.worktreeHandle;
36580
+ if (handle) out.push({ id, handle });
36581
+ };
36582
+ for (const depId of node.deps) visit(depId);
36583
+ return out;
36584
+ }
36585
+ /**
36586
+ * Sequentially merge every deferred worktree this node covers (in
36587
+ * ancestors-first order) into parent HEAD. Nodes without a recorded worktree
36588
+ * handle (worktree isolation disabled, or a read-only node) are a no-op. On
36589
+ * conflict the branch is kept and the conflict is surfaced in the merge
36590
+ * node's error — remaining sources still attempt to merge (independent
36591
+ * branches shouldn't be blocked by one conflict).
35913
36592
  */
35914
36593
  async runMergeNode(node, graph) {
35915
36594
  const conflicts = [];
35916
36595
  const merged = [];
35917
- for (const depId of node.deps) {
35918
- const state3 = this.nodeRunState.get(depId);
35919
- const handle = state3?.worktreeHandle;
35920
- if (!handle) continue;
36596
+ for (const { id: depId, handle } of this.collectWorktreeSources(node, graph)) {
35921
36597
  let result;
35922
36598
  try {
35923
36599
  result = await this.mergeFn(handle, {
@@ -35935,6 +36611,7 @@ var init_executor = __esm({
35935
36611
  if (!result.ok) {
35936
36612
  conflicts.push(`${depId}: ${result.message}`);
35937
36613
  } else {
36614
+ this.nodeRunState.set(depId, { worktreeHandle: null });
35938
36615
  merged.push(depId);
35939
36616
  }
35940
36617
  }
@@ -35962,9 +36639,31 @@ var init_executor = __esm({
35962
36639
  node.status = "done";
35963
36640
  node.result = res.result;
35964
36641
  this.radio("node_end", { description: node.label, agent: node.kind, ok: true });
36642
+ this.reconcileRepairedNode(graph, node);
36643
+ if (node.kind === "verify") this.applyVerifyVerdict(graph, node);
35965
36644
  return;
35966
36645
  }
35967
36646
  node.error = res.error;
36647
+ if (this.aborted) {
36648
+ node.status = "error";
36649
+ this.radio("node_end", {
36650
+ description: node.label,
36651
+ agent: node.kind,
36652
+ detail: res.error,
36653
+ ok: false
36654
+ });
36655
+ return;
36656
+ }
36657
+ if (res.cancelled === false) {
36658
+ node.status = "error";
36659
+ this.radio("node_end", {
36660
+ description: node.label,
36661
+ agent: node.kind,
36662
+ detail: res.error,
36663
+ ok: false
36664
+ });
36665
+ return;
36666
+ }
35968
36667
  if (node.retryCount < node.maxRetries) {
35969
36668
  node.retryCount += 1;
35970
36669
  node.status = "pending";
@@ -35997,6 +36696,180 @@ var init_executor = __esm({
35997
36696
  ok: false
35998
36697
  });
35999
36698
  }
36699
+ /**
36700
+ * A `fix` node just completed the work its failed predecessor could not.
36701
+ * That unit of work IS done — but the predecessor was left terminally
36702
+ * `error`, and since `isConverged` requires every node to be `done`/
36703
+ * `skipped`, a fully repaired graph reported "did not converge" and listed
36704
+ * the repaired node under `failedNodeIds`. The cross-run snapshot then told
36705
+ * the next planner to redo work the fix had already completed.
36706
+ *
36707
+ * Marking it `done` has no scheduling effect (dependents were re-pointed at
36708
+ * the fix when it was spawned) — it is purely how the run is reported. The
36709
+ * original failure stays visible as the separate `fix: …` node and in the
36710
+ * repaired node's result line.
36711
+ */
36712
+ reconcileRepairedNode(graph, fixNode) {
36713
+ const failedId = this.repairs.get(fixNode.id);
36714
+ if (!failedId) return;
36715
+ const failed = graph.nodes.get(failedId);
36716
+ if (!failed || failed.status !== "error") return;
36717
+ const original = failed.error ? ` (original failure: ${failed.error})` : "";
36718
+ failed.status = "done";
36719
+ failed.result = `repaired by "${fixNode.label}"${original}${fixNode.result ? `: ${fixNode.result}` : ""}`;
36720
+ failed.error = void 0;
36721
+ this.radio("node_end", {
36722
+ description: failed.label,
36723
+ agent: failed.kind,
36724
+ detail: `repaired by ${fixNode.id}`,
36725
+ ok: true
36726
+ });
36727
+ }
36728
+ /**
36729
+ * Act on what a completed `verify` node concluded.
36730
+ *
36731
+ * The verify itself stays `done` either way — it did its job, and doing it
36732
+ * well means being free to say "no". A FAIL instead sends the WRITER back
36733
+ * through a bounded rework round.
36734
+ *
36735
+ * Without this the verdict text was never read: a verify that reported the
36736
+ * work as wrong was recorded exactly like one that reported it correct, the
36737
+ * graph converged over the defect, and the only iteration the engine could
36738
+ * do was on execution failure. An `unknown` verdict (no parseable trailer)
36739
+ * is deliberately non-blocking — a prompt drift must not be able to wedge
36740
+ * every graph — but it is recorded, because a gate that has silently stopped
36741
+ * working is worse than no gate.
36742
+ */
36743
+ applyVerifyVerdict(graph, verify) {
36744
+ const { verdict, findings } = parseVerifyVerdict(verify.result);
36745
+ if (verdict === "pass") return;
36746
+ const writer = this.writerBehind(verify, graph);
36747
+ if (!writer) return;
36748
+ if (verdict === "unknown") {
36749
+ this.unresolved.push({
36750
+ nodeId: writer.id,
36751
+ label: writer.label,
36752
+ reason: "unknown",
36753
+ findings: findings || "(verify produced no parseable VERDICT line)"
36754
+ });
36755
+ return;
36756
+ }
36757
+ const root = this.reviewLineage.get(writer.id) ?? writer.id;
36758
+ const spent = this.reviewRounds.get(root) ?? 0;
36759
+ if (this.aborted || spent >= this.maxReviewRounds) {
36760
+ this.unresolved.push({
36761
+ nodeId: writer.id,
36762
+ label: writer.label,
36763
+ reason: "fail",
36764
+ findings
36765
+ });
36766
+ writer.result = `${writer.result ?? ""}
36767
+
36768
+ [accepted with unresolved verify findings from ` + `"${verify.label}"]${findings ? `: ${firstLine(findings)}` : ""}`.trim();
36769
+ this.radio("node_end", {
36770
+ description: writer.label,
36771
+ agent: writer.kind,
36772
+ detail: this.aborted ? "verify FAIL left unresolved (run cancelled)" : `verify FAIL left unresolved (rework budget ${this.maxReviewRounds} spent)`,
36773
+ ok: false
36774
+ });
36775
+ return;
36776
+ }
36777
+ this.reviewRounds.set(root, spent + 1);
36778
+ this.spawnReworkPair(graph, writer, verify, findings, root, spent + 1);
36779
+ }
36780
+ /**
36781
+ * The writer whose work a `verify` node judged.
36782
+ *
36783
+ * Walks up through non-writer deps, the same shape `collectWorktreeSources`
36784
+ * relies on: a verify's dep is normally its writer directly, but after a
36785
+ * rework round the chain is writer → verify → rework → verify, and the
36786
+ * rework (a `fix` node) is itself the writer to send back.
36787
+ */
36788
+ writerBehind(verify, graph) {
36789
+ const seen = /* @__PURE__ */ new Set();
36790
+ const visit = (id) => {
36791
+ if (seen.has(id)) return void 0;
36792
+ seen.add(id);
36793
+ const n = graph.nodes.get(id);
36794
+ if (!n || n.kind === "merge") return void 0;
36795
+ if (n.kind === "general" || n.kind === "fix") return n;
36796
+ for (const dep of n.deps) {
36797
+ const found = visit(dep);
36798
+ if (found) return found;
36799
+ }
36800
+ return void 0;
36801
+ };
36802
+ for (const depId of verify.deps) {
36803
+ const found = visit(depId);
36804
+ if (found) return found;
36805
+ }
36806
+ return void 0;
36807
+ }
36808
+ /**
36809
+ * Send a writer's work back for one more round: a rework node carrying the
36810
+ * verify's findings, plus a fresh verify to judge the result.
36811
+ *
36812
+ * The rework runs INSIDE the writer's worktree instead of creating one of
36813
+ * its own. Two worktrees for one scope means two branches, and the merge
36814
+ * node walks up to the writer — so a rework on its own branch would be
36815
+ * merged never or twice, exactly the stranded-work failure the merge fix
36816
+ * addressed. `allowWorktree: false` on this node's deps suppresses creation,
36817
+ * and `cwdOverride` (resolved via {@link inheritedWorktreeCwdFor}) points it
36818
+ * at the existing tree; the handle stays registered against the writer.
36819
+ *
36820
+ * Acyclicity is preserved by construction: both new nodes point only at
36821
+ * nodes that already exist, and the rewiring moves an existing edge forward
36822
+ * along the chain rather than back into it.
36823
+ */
36824
+ spawnReworkPair(graph, writer, verify, findings, root, round) {
36825
+ const reworkId = `rework-${root}-${round}`;
36826
+ const reworkNode = {
36827
+ id: reworkId,
36828
+ kind: "fix",
36829
+ label: `rework: ${writer.label}`,
36830
+ prompt: `A reviewer inspected this work on disk and REJECTED it. Address every finding below, then leave the work in a state that satisfies the original task.
36831
+
36832
+ ## Original task
36833
+ ${writer.prompt}
36834
+
36835
+ ## Reviewer findings (these are what must change)
36836
+ ${findings || "(the reviewer reported FAIL without detail)"}`,
36837
+ ...writer.scope ? { scope: writer.scope } : {},
36838
+ ...writer.acceptance ? { acceptance: writer.acceptance } : {},
36839
+ // The verify is already `done`, so the rework is immediately ready.
36840
+ deps: [verify.id],
36841
+ status: "pending",
36842
+ retryCount: 0,
36843
+ maxRetries: 0
36844
+ };
36845
+ graph.nodes.set(reworkId, reworkNode);
36846
+ this.reworks.set(reworkId, writer.id);
36847
+ this.reviewLineage.set(reworkId, root);
36848
+ const reVerifyId = `verify-${reworkId}`;
36849
+ const reVerifyNode = {
36850
+ id: reVerifyId,
36851
+ kind: "verify",
36852
+ label: `verify: ${writer.label} (rework ${round})`,
36853
+ prompt: verify.prompt,
36854
+ deps: [reworkId],
36855
+ status: "pending",
36856
+ retryCount: 0,
36857
+ maxRetries: verify.maxRetries
36858
+ };
36859
+ graph.nodes.set(reVerifyId, reVerifyNode);
36860
+ for (const other of graph.nodes.values()) {
36861
+ if (other.id === reworkId || other.id === reVerifyId) continue;
36862
+ if (other.deps.includes(verify.id)) {
36863
+ other.deps = other.deps.map((d) => d === verify.id ? reVerifyId : d);
36864
+ }
36865
+ }
36866
+ this.radio("node_fix", {
36867
+ description: reworkNode.label,
36868
+ agent: "fix",
36869
+ detail: `verify FAIL on "${writer.label}" \u2014 rework round ${round}/${this.maxReviewRounds}`,
36870
+ ok: false
36871
+ });
36872
+ }
36000
36873
  /**
36001
36874
  * Create a `fix` node that attempts to redo the failed node's work, wired
36002
36875
  * so downstream dependents of the failed node also wait on the fix.
@@ -36023,6 +36896,7 @@ ${failed.error ?? "unknown error"}`,
36023
36896
  // no further retries — one fix attempt per failed node in v1
36024
36897
  };
36025
36898
  graph.nodes.set(fixId, fixNode);
36899
+ this.repairs.set(fixId, failed.id);
36026
36900
  for (const other of graph.nodes.values()) {
36027
36901
  if (other.id === fixId) continue;
36028
36902
  if (other.deps.includes(failed.id)) {
@@ -36453,10 +37327,10 @@ var init_prereqChecks = __esm({
36453
37327
 
36454
37328
  // src/cli/plugins/prefs.ts
36455
37329
  import { existsSync as existsSync36, readFileSync as readFileSync29, writeFileSync as writeFileSync18, mkdirSync as mkdirSync15 } from "node:fs";
36456
- import path35 from "node:path";
37330
+ import path36 from "node:path";
36457
37331
  import os9 from "node:os";
36458
37332
  function getPluginPrefsPath() {
36459
- return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path35.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
37333
+ return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path36.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
36460
37334
  }
36461
37335
  function getPluginPrefs() {
36462
37336
  const file2 = getPluginPrefsPath();
@@ -36477,7 +37351,7 @@ function getPluginPrefs() {
36477
37351
  }
36478
37352
  function writePluginPrefs(prefs) {
36479
37353
  const file2 = getPluginPrefsPath();
36480
- mkdirSync15(path35.dirname(file2), { recursive: true });
37354
+ mkdirSync15(path36.dirname(file2), { recursive: true });
36481
37355
  writeFileSync18(file2, JSON.stringify(prefs, null, 2), {
36482
37356
  encoding: "utf-8",
36483
37357
  mode: 384
@@ -36514,7 +37388,7 @@ __export(registry_exports, {
36514
37388
  isBinaryOnPath: () => isBinaryOnPath
36515
37389
  });
36516
37390
  import { existsSync as existsSync37 } from "node:fs";
36517
- import path36 from "node:path";
37391
+ import path37 from "node:path";
36518
37392
  function detectLocalBin(bin) {
36519
37393
  return (cwd) => {
36520
37394
  try {
@@ -36532,7 +37406,7 @@ function isBinaryOnPath(bin, opts = {}) {
36532
37406
  const platform = opts.platform ?? process.platform;
36533
37407
  const exists = opts.exists ?? existsSync37;
36534
37408
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
36535
- const pathMod = platform === "win32" ? path36.win32 : path36.posix;
37409
+ const pathMod = platform === "win32" ? path37.win32 : path37.posix;
36536
37410
  const sep2 = platform === "win32" ? ";" : ":";
36537
37411
  const dirs = pathEnv.split(sep2).filter((d) => d.length > 0);
36538
37412
  const candidates = [bin];
@@ -36841,10 +37715,10 @@ __export(triggerLock_exports, {
36841
37715
  lockPath: () => lockPath,
36842
37716
  releaseLock: () => releaseLock
36843
37717
  });
36844
- import { promises as fs23 } from "node:fs";
36845
- import * as path41 from "node:path";
37718
+ import { promises as fs24 } from "node:fs";
37719
+ import * as path42 from "node:path";
36846
37720
  function lockPath(projectRoot) {
36847
- return path41.join(projectRoot, ".zelari", "trigger.lock");
37721
+ return path42.join(projectRoot, ".zelari", "trigger.lock");
36848
37722
  }
36849
37723
  function isPidAlive(pid) {
36850
37724
  try {
@@ -36857,10 +37731,10 @@ function isPidAlive(pid) {
36857
37731
  }
36858
37732
  async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date()) {
36859
37733
  const lp = lockPath(projectRoot);
36860
- const dir = path41.dirname(lp);
36861
- await fs23.mkdir(dir, { recursive: true });
37734
+ const dir = path42.dirname(lp);
37735
+ await fs24.mkdir(dir, { recursive: true });
36862
37736
  try {
36863
- const raw = await fs23.readFile(lp, "utf8");
37737
+ const raw = await fs24.readFile(lp, "utf8");
36864
37738
  const existing = JSON.parse(raw);
36865
37739
  if (existing.pid && isPidAlive(existing.pid)) {
36866
37740
  return { acquired: false, heldBy: existing.pid, lockPath: lp };
@@ -36871,13 +37745,13 @@ async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date())
36871
37745
  pid: process.pid,
36872
37746
  acquiredAt: now().toISOString()
36873
37747
  };
36874
- await fs23.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
37748
+ await fs24.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
36875
37749
  return { acquired: true, lockPath: lp };
36876
37750
  }
36877
37751
  async function releaseLock(projectRoot) {
36878
37752
  const lp = lockPath(projectRoot);
36879
37753
  try {
36880
- await fs23.unlink(lp);
37754
+ await fs24.unlink(lp);
36881
37755
  } catch {
36882
37756
  }
36883
37757
  }
@@ -37165,12 +38039,12 @@ function ensureHome() {
37165
38039
  }
37166
38040
  }
37167
38041
  function loadCompanionConfig() {
37168
- const path43 = getCompanionConfigPath();
37169
- if (!existsSync43(path43)) {
38042
+ const path44 = getCompanionConfigPath();
38043
+ if (!existsSync43(path44)) {
37170
38044
  return { projects: [] };
37171
38045
  }
37172
38046
  try {
37173
- const raw = JSON.parse(readFileSync34(path43, "utf8"));
38047
+ const raw = JSON.parse(readFileSync34(path44, "utf8"));
37174
38048
  const projects = Array.isArray(raw.projects) ? raw.projects.filter(
37175
38049
  (p3) => p3 && typeof p3.path === "string" && p3.path.trim() && typeof (p3.id ?? p3.name) === "string"
37176
38050
  ).map((p3) => ({
@@ -37208,16 +38082,16 @@ function loadOrCreateToken(explicit) {
37208
38082
  return { token: explicit.trim(), created: false };
37209
38083
  }
37210
38084
  ensureHome();
37211
- const path43 = getCompanionTokenPath();
37212
- if (existsSync43(path43)) {
37213
- const t = readFileSync34(path43, "utf8").trim();
38085
+ const path44 = getCompanionTokenPath();
38086
+ if (existsSync43(path44)) {
38087
+ const t = readFileSync34(path44, "utf8").trim();
37214
38088
  if (t) return { token: t, created: false };
37215
38089
  }
37216
38090
  const token = randomBytes3(24).toString("base64url");
37217
- writeFileSync21(path43, token + "\n", "utf8");
38091
+ writeFileSync21(path44, token + "\n", "utf8");
37218
38092
  try {
37219
- const fs24 = __require("node:fs");
37220
- fs24.chmodSync?.(path43, 384);
38093
+ const fs25 = __require("node:fs");
38094
+ fs25.chmodSync?.(path44, 384);
37221
38095
  } catch {
37222
38096
  }
37223
38097
  return { token, created: true };
@@ -37242,17 +38116,17 @@ function mergeProjects(cfg, extraPaths) {
37242
38116
  byId.set(p3.id, p3);
37243
38117
  }
37244
38118
  for (const raw of extraPaths) {
37245
- const path43 = raw.trim();
37246
- if (!path43) continue;
37247
- let id = slugFromPath(path43);
38119
+ const path44 = raw.trim();
38120
+ if (!path44) continue;
38121
+ let id = slugFromPath(path44);
37248
38122
  let n = 2;
37249
- while (byId.has(id) && byId.get(id).path !== path43) {
37250
- id = `${slugFromPath(path43)}-${n++}`;
38123
+ while (byId.has(id) && byId.get(id).path !== path44) {
38124
+ id = `${slugFromPath(path44)}-${n++}`;
37251
38125
  }
37252
38126
  byId.set(id, {
37253
38127
  id,
37254
- name: slugFromPath(path43),
37255
- path: path43
38128
+ name: slugFromPath(path44),
38129
+ path: path44
37256
38130
  });
37257
38131
  }
37258
38132
  return [...byId.values()];
@@ -37629,9 +38503,9 @@ async function runCompanionServe(opts = {}) {
37629
38503
  return;
37630
38504
  }
37631
38505
  const url2 = parseUrl(req);
37632
- const path43 = url2.pathname.replace(/\/+$/, "") || "/";
38506
+ const path44 = url2.pathname.replace(/\/+$/, "") || "/";
37633
38507
  try {
37634
- if (req.method === "GET" && (path43 === "/health" || path43 === "/v1/health")) {
38508
+ if (req.method === "GET" && (path44 === "/health" || path44 === "/v1/health")) {
37635
38509
  sendJson(res, 200, {
37636
38510
  ok: true,
37637
38511
  service: "zelari-companion",
@@ -37643,18 +38517,18 @@ async function runCompanionServe(opts = {}) {
37643
38517
  });
37644
38518
  return;
37645
38519
  }
37646
- if (path43.startsWith("/v1")) {
38520
+ if (path44.startsWith("/v1")) {
37647
38521
  if (!tokenMatches(token, getBearer(req))) {
37648
38522
  sendJson(res, 401, { ok: false, error: "unauthorized" });
37649
38523
  return;
37650
38524
  }
37651
38525
  }
37652
- if (req.method === "GET" && path43 === "/v1/config") {
38526
+ if (req.method === "GET" && path44 === "/v1/config") {
37653
38527
  const snap = buildDesktopConfigSnapshot();
37654
38528
  sendJson(res, 200, { ok: true, ...snap });
37655
38529
  return;
37656
38530
  }
37657
- if (req.method === "GET" && path43 === "/v1/projects") {
38531
+ if (req.method === "GET" && path44 === "/v1/projects") {
37658
38532
  sendJson(res, 200, {
37659
38533
  ok: true,
37660
38534
  projects: projects.map((p3) => ({
@@ -37665,7 +38539,7 @@ async function runCompanionServe(opts = {}) {
37665
38539
  });
37666
38540
  return;
37667
38541
  }
37668
- if (req.method === "GET" && path43 === "/v1/runs") {
38542
+ if (req.method === "GET" && path44 === "/v1/runs") {
37669
38543
  sendJson(res, 200, {
37670
38544
  ok: true,
37671
38545
  active: runs.getActive(),
@@ -37683,7 +38557,7 @@ async function runCompanionServe(opts = {}) {
37683
38557
  });
37684
38558
  return;
37685
38559
  }
37686
- if (req.method === "POST" && path43 === "/v1/runs") {
38560
+ if (req.method === "POST" && path44 === "/v1/runs") {
37687
38561
  const raw = await readBody(req);
37688
38562
  let body = {};
37689
38563
  try {
@@ -37730,7 +38604,7 @@ async function runCompanionServe(opts = {}) {
37730
38604
  });
37731
38605
  return;
37732
38606
  }
37733
- const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path43);
38607
+ const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path44);
37734
38608
  if (req.method === "GET" && eventsMatch) {
37735
38609
  const runId = eventsMatch[1];
37736
38610
  const run = runs.getRun(runId);
@@ -37795,7 +38669,7 @@ async function runCompanionServe(opts = {}) {
37795
38669
  }, 500);
37796
38670
  return;
37797
38671
  }
37798
- const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path43);
38672
+ const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path44);
37799
38673
  if (req.method === "POST" && cancelMatch) {
37800
38674
  const runId = cancelMatch[1];
37801
38675
  const result = runs.cancel(runId);
@@ -37907,11 +38781,11 @@ import { execSync as execSync2 } from "node:child_process";
37907
38781
  import { existsSync as existsSync45, readFileSync as readFileSync35, readlinkSync, statSync as statSync7 } from "node:fs";
37908
38782
  import { createRequire as createRequire3 } from "node:module";
37909
38783
  import { fileURLToPath as fileURLToPath2 } from "node:url";
37910
- import path42 from "node:path";
38784
+ import path43 from "node:path";
37911
38785
  function findPackageRoot(start) {
37912
38786
  let dir = start;
37913
38787
  for (let i = 0; i < 6; i += 1) {
37914
- const candidate = path42.join(dir, "package.json");
38788
+ const candidate = path43.join(dir, "package.json");
37915
38789
  if (existsSync45(candidate)) {
37916
38790
  try {
37917
38791
  const pkg = JSON.parse(readFileSync35(candidate, "utf8"));
@@ -37919,11 +38793,11 @@ function findPackageRoot(start) {
37919
38793
  } catch {
37920
38794
  }
37921
38795
  }
37922
- const parent = path42.dirname(dir);
38796
+ const parent = path43.dirname(dir);
37923
38797
  if (parent === dir) break;
37924
38798
  dir = parent;
37925
38799
  }
37926
- return path42.resolve(__dirname3, "..", "..", "..");
38800
+ return path43.resolve(__dirname3, "..", "..", "..");
37927
38801
  }
37928
38802
  function tryExec(cmd) {
37929
38803
  try {
@@ -37937,7 +38811,7 @@ function tryExec(cmd) {
37937
38811
  }
37938
38812
  function readPackageJson3() {
37939
38813
  try {
37940
- const pkgPath = path42.join(packageRoot, "package.json");
38814
+ const pkgPath = path43.join(packageRoot, "package.json");
37941
38815
  return JSON.parse(readFileSync35(pkgPath, "utf8"));
37942
38816
  } catch {
37943
38817
  return null;
@@ -37953,7 +38827,7 @@ function checkShim(pkgName) {
37953
38827
  }
37954
38828
  const isWin = process.platform === "win32";
37955
38829
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
37956
- const shimPath = path42.join(prefix, shimName);
38830
+ const shimPath = path43.join(prefix, shimName);
37957
38831
  if (!existsSync45(shimPath)) {
37958
38832
  return FAIL(
37959
38833
  `shim not found at ${shimPath}
@@ -37981,8 +38855,8 @@ function checkShim(pkgName) {
37981
38855
  fix: npm install -g ${pkgName}@latest --force`
37982
38856
  );
37983
38857
  }
37984
- const resolved = path42.resolve(path42.dirname(shimPath), target);
37985
- const expected = path42.join(
38858
+ const resolved = path43.resolve(path43.dirname(shimPath), target);
38859
+ const expected = path43.join(
37986
38860
  prefix,
37987
38861
  "node_modules",
37988
38862
  pkgName,
@@ -38021,7 +38895,7 @@ function checkNode(pkg) {
38021
38895
  return OK(`node ${raw}`);
38022
38896
  }
38023
38897
  function checkBundle() {
38024
- const bundle = path42.join(packageRoot, "dist", "cli", "main.bundled.js");
38898
+ const bundle = path43.join(packageRoot, "dist", "cli", "main.bundled.js");
38025
38899
  if (!existsSync45(bundle)) {
38026
38900
  return FAIL(
38027
38901
  `dist/cli/main.bundled.js missing at ${bundle}
@@ -38042,7 +38916,7 @@ function checkRuntimeDeps() {
38042
38916
  const missing = [];
38043
38917
  for (const dep of required2) {
38044
38918
  try {
38045
- const localReq = createRequire3(path42.join(packageRoot, "package.json"));
38919
+ const localReq = createRequire3(path43.join(packageRoot, "package.json"));
38046
38920
  localReq.resolve(dep);
38047
38921
  } catch {
38048
38922
  missing.push(dep);
@@ -38218,7 +39092,7 @@ var init_doctor = __esm({
38218
39092
  "use strict";
38219
39093
  init_prereqChecks();
38220
39094
  require3 = createRequire3(import.meta.url);
38221
- __dirname3 = path42.dirname(fileURLToPath2(import.meta.url));
39095
+ __dirname3 = path43.dirname(fileURLToPath2(import.meta.url));
38222
39096
  packageRoot = findPackageRoot(__dirname3);
38223
39097
  OK = (message) => ({
38224
39098
  ok: true,
@@ -44633,6 +45507,7 @@ init_krakenRadio();
44633
45507
  init_auditLogger();
44634
45508
  init_toolRegistry();
44635
45509
  init_planner();
45510
+ init_graphMemory();
44636
45511
  init_executor();
44637
45512
  init_graphStatus();
44638
45513
  async function handleKrakenGraph(ctx, prompt) {
@@ -44657,19 +45532,36 @@ async function handleKrakenGraph(ctx, prompt) {
44657
45532
  })
44658
45533
  };
44659
45534
  try {
44660
- const graph = await planTaskGraph({ prompt, graphId: `kraken-${Date.now().toString(36)}` });
45535
+ const previous = await loadGraphSnapshot(ctx.cwd);
45536
+ const previousAttempt = formatSnapshotForPlanner(previous);
45537
+ if (previousAttempt) {
45538
+ appendSystem(ctx.setMessages, "[kraken] resuming from the previous unfinished graph");
45539
+ }
45540
+ const graph = await planTaskGraph({
45541
+ prompt,
45542
+ graphId: `kraken-${Date.now().toString(36)}`,
45543
+ cwd: ctx.cwd,
45544
+ ...previousAttempt ? { previousAttempt } : {}
45545
+ });
44661
45546
  appendSystem(ctx.setMessages, formatKrakenGraphAscii(graph));
44662
45547
  const executor = new KrakenGraphExecutor({
44663
45548
  taskToolDeps,
44664
45549
  parentCwd: ctx.cwd,
44665
- sessionId: ctx.sessionId
45550
+ sessionId: ctx.sessionId,
45551
+ goal: prompt
44666
45552
  });
44667
45553
  const summary = await executor.execute(graph);
45554
+ const digest = formatKrakenGraphDigest(summary.graph, {
45555
+ durationsMs: summary.durationsMs,
45556
+ unresolvedFindings: summary.unresolvedFindings
45557
+ });
44668
45558
  appendSystem(
44669
45559
  ctx.setMessages,
44670
45560
  `${formatKrakenGraphAscii(summary.graph)}
44671
45561
 
44672
- ` + (summary.converged ? "[kraken] graph converged." : `[kraken] graph did not converge \u2014 failed: ${summary.failedNodeIds.join(", ") || "none"}`)
45562
+ ${digest}
45563
+
45564
+ ` + (summary.converged ? "[kraken] graph converged." : summary.cancelled ? "[kraken] graph cancelled." : `[kraken] graph did not converge \u2014 failed: ${summary.failedNodeIds.join(", ") || "none"}`)
44673
45565
  );
44674
45566
  } catch (err) {
44675
45567
  appendSystem(
@@ -44976,17 +45868,17 @@ ${result.output.split("\n").slice(-8).join("\n")}` : "";
44976
45868
  }
44977
45869
 
44978
45870
  // src/cli/slashHandlers/promoteMember.ts
44979
- import { promises as fs19 } from "node:fs";
44980
- import path37 from "node:path";
45871
+ import { promises as fs20 } from "node:fs";
45872
+ import path38 from "node:path";
44981
45873
  import os10 from "node:os";
44982
45874
  async function handlePromoteMember(ctx, memberId) {
44983
45875
  try {
44984
45876
  const { promoteMember: promoteMember2 } = await Promise.resolve().then(() => (init_council(), council_exports));
44985
45877
  const { skill, markdown } = promoteMember2(memberId);
44986
- const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path37.join(os10.homedir(), ".tmp", "zelari-code", "skills");
44987
- await fs19.mkdir(skillDir, { recursive: true });
44988
- const filePath = path37.join(skillDir, `${skill.id}.md`);
44989
- await fs19.writeFile(filePath, markdown, "utf8");
45878
+ const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path38.join(os10.homedir(), ".tmp", "zelari-code", "skills");
45879
+ await fs20.mkdir(skillDir, { recursive: true });
45880
+ const filePath = path38.join(skillDir, `${skill.id}.md`);
45881
+ await fs20.writeFile(filePath, markdown, "utf8");
44990
45882
  appendSystem(
44991
45883
  ctx.setMessages,
44992
45884
  `[promote-member] ${skill.name} (${memberId}) \u2192 ${filePath}
@@ -45002,25 +45894,25 @@ async function handlePromoteMember(ctx, memberId) {
45002
45894
  }
45003
45895
 
45004
45896
  // src/cli/branchManager.ts
45005
- import { promises as fs20, existsSync as existsSync38, readFileSync as readFileSync30, writeFileSync as writeFileSync19, mkdirSync as mkdirSync16, statSync as statSync4, rmSync as rmSync3 } from "node:fs";
45006
- import path38 from "node:path";
45897
+ import { promises as fs21, existsSync as existsSync38, readFileSync as readFileSync30, writeFileSync as writeFileSync19, mkdirSync as mkdirSync16, statSync as statSync4, rmSync as rmSync3 } from "node:fs";
45898
+ import path39 from "node:path";
45007
45899
  import os11 from "node:os";
45008
45900
  var META_FILENAME = "meta.json";
45009
45901
  var SESSIONS_SUBDIR = "sessions";
45010
45902
  function getBranchesBaseDir() {
45011
- return process.env.ANATHEMA_BRANCHES_DIR ?? path38.join(os11.homedir(), ".tmp", "zelari-code", "branches");
45903
+ return process.env.ANATHEMA_BRANCHES_DIR ?? path39.join(os11.homedir(), ".tmp", "zelari-code", "branches");
45012
45904
  }
45013
45905
  function getSessionsBaseDir() {
45014
- return process.env.ANATHEMA_SESSIONS_DIR ?? path38.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
45906
+ return process.env.ANATHEMA_SESSIONS_DIR ?? path39.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
45015
45907
  }
45016
45908
  function branchPathFor(name, baseDir) {
45017
- return path38.join(baseDir, name);
45909
+ return path39.join(baseDir, name);
45018
45910
  }
45019
45911
  function metaPathFor(name, baseDir) {
45020
- return path38.join(baseDir, name, META_FILENAME);
45912
+ return path39.join(baseDir, name, META_FILENAME);
45021
45913
  }
45022
45914
  function sessionsPathFor(name, baseDir) {
45023
- return path38.join(baseDir, name, SESSIONS_SUBDIR);
45915
+ return path39.join(baseDir, name, SESSIONS_SUBDIR);
45024
45916
  }
45025
45917
  function readBranchMeta(name, baseDir) {
45026
45918
  const metaPath = metaPathFor(name, baseDir);
@@ -45045,13 +45937,13 @@ function readBranchMeta(name, baseDir) {
45045
45937
  }
45046
45938
  function writeBranchMeta(name, baseDir, meta3) {
45047
45939
  const metaPath = metaPathFor(name, baseDir);
45048
- mkdirSync16(path38.dirname(metaPath), { recursive: true });
45940
+ mkdirSync16(path39.dirname(metaPath), { recursive: true });
45049
45941
  writeFileSync19(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
45050
45942
  }
45051
45943
  async function countSessions(name, baseDir) {
45052
45944
  const sessionsPath = sessionsPathFor(name, baseDir);
45053
45945
  try {
45054
- const entries = await fs20.readdir(sessionsPath);
45946
+ const entries = await fs21.readdir(sessionsPath);
45055
45947
  return entries.filter((e) => e.endsWith(".jsonl")).length;
45056
45948
  } catch (err) {
45057
45949
  if (err.code === "ENOENT") return 0;
@@ -45096,15 +45988,15 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
45096
45988
  if (branchExists(name, baseDir)) {
45097
45989
  throw new BranchAlreadyExistsError(name);
45098
45990
  }
45099
- const sourcePath = path38.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
45991
+ const sourcePath = path39.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
45100
45992
  if (!existsSync38(sourcePath)) {
45101
45993
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
45102
45994
  }
45103
45995
  const branchPath = branchPathFor(name, baseDir);
45104
45996
  const branchSessionsPath = sessionsPathFor(name, baseDir);
45105
45997
  mkdirSync16(branchSessionsPath, { recursive: true });
45106
- const destPath = path38.join(branchSessionsPath, `${fromSessionId}.jsonl`);
45107
- await fs20.copyFile(sourcePath, destPath);
45998
+ const destPath = path39.join(branchSessionsPath, `${fromSessionId}.jsonl`);
45999
+ await fs21.copyFile(sourcePath, destPath);
45108
46000
  const meta3 = {
45109
46001
  name,
45110
46002
  createdAt: Date.now(),
@@ -45122,7 +46014,7 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
45122
46014
  async function listBranches(baseDir = getBranchesBaseDir()) {
45123
46015
  let entries;
45124
46016
  try {
45125
- entries = await fs20.readdir(baseDir);
46017
+ entries = await fs21.readdir(baseDir);
45126
46018
  } catch (err) {
45127
46019
  if (err.code === "ENOENT") return [];
45128
46020
  throw err;
@@ -45203,26 +46095,26 @@ async function handleBranchCheckout(ctx, branchName) {
45203
46095
  }
45204
46096
 
45205
46097
  // src/cli/slashHandlers/workspace.ts
45206
- import { promises as fs21 } from "node:fs";
45207
- import path39 from "node:path";
46098
+ import { promises as fs22 } from "node:fs";
46099
+ import path40 from "node:path";
45208
46100
  async function handleWorkspaceShow(ctx, what) {
45209
46101
  try {
45210
- const zelari = path39.join(process.cwd(), ".zelari");
46102
+ const zelari = path40.join(process.cwd(), ".zelari");
45211
46103
  let content;
45212
46104
  switch (what) {
45213
46105
  case "plan": {
45214
- const planPath = path39.join(zelari, "plan.md");
46106
+ const planPath = path40.join(zelari, "plan.md");
45215
46107
  try {
45216
- content = await fs21.readFile(planPath, "utf-8");
46108
+ content = await fs22.readFile(planPath, "utf-8");
45217
46109
  } catch {
45218
46110
  content = "(no plan.md yet \u2014 run a council session first)";
45219
46111
  }
45220
46112
  break;
45221
46113
  }
45222
46114
  case "decisions": {
45223
- const decisionsDir = path39.join(zelari, "decisions");
46115
+ const decisionsDir = path40.join(zelari, "decisions");
45224
46116
  try {
45225
- const files = (await fs21.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
46117
+ const files = (await fs22.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
45226
46118
  if (files.length === 0) {
45227
46119
  content = "(no ADRs yet \u2014 invoke /council to generate some)";
45228
46120
  } else {
@@ -45230,7 +46122,7 @@ async function handleWorkspaceShow(ctx, what) {
45230
46122
  `];
45231
46123
  const { parseFrontmatter: parseFrontmatter2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
45232
46124
  for (const f of files) {
45233
- const raw = await fs21.readFile(path39.join(decisionsDir, f), "utf-8");
46125
+ const raw = await fs22.readFile(path40.join(decisionsDir, f), "utf-8");
45234
46126
  const { meta: meta3, body } = parseFrontmatter2(raw);
45235
46127
  const title = meta3.title ?? body.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? f;
45236
46128
  lines.push(`- **${f.replace(/\.md$/, "")}** [${meta3.status ?? "unknown"}] ${title}`);
@@ -45243,27 +46135,27 @@ async function handleWorkspaceShow(ctx, what) {
45243
46135
  break;
45244
46136
  }
45245
46137
  case "risks": {
45246
- const risksPath = path39.join(zelari, "risks.md");
46138
+ const risksPath = path40.join(zelari, "risks.md");
45247
46139
  try {
45248
- content = await fs21.readFile(risksPath, "utf-8");
46140
+ content = await fs22.readFile(risksPath, "utf-8");
45249
46141
  } catch {
45250
46142
  content = "(no risks.md yet)";
45251
46143
  }
45252
46144
  break;
45253
46145
  }
45254
46146
  case "agents": {
45255
- const agentsPath = path39.join(process.cwd(), "AGENTS.MD");
46147
+ const agentsPath = path40.join(process.cwd(), "AGENTS.MD");
45256
46148
  try {
45257
- content = await fs21.readFile(agentsPath, "utf-8");
46149
+ content = await fs22.readFile(agentsPath, "utf-8");
45258
46150
  } catch {
45259
46151
  content = "(no AGENTS.MD yet at project root \u2014 run `/workspace sync` after a council session)";
45260
46152
  }
45261
46153
  break;
45262
46154
  }
45263
46155
  case "docs": {
45264
- const docsDir = path39.join(zelari, "docs");
46156
+ const docsDir = path40.join(zelari, "docs");
45265
46157
  try {
45266
- const files = (await fs21.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
46158
+ const files = (await fs22.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
45267
46159
  content = files.length ? `# Docs (${files.length})
45268
46160
 
45269
46161
  ` + files.map((f) => `- ${f}`).join("\n") : "(no docs drafts yet)";
@@ -45303,8 +46195,8 @@ async function handleWorkspaceReset(ctx, force) {
45303
46195
  return;
45304
46196
  }
45305
46197
  try {
45306
- const target = path39.join(process.cwd(), ".zelari");
45307
- await fs21.rm(target, { recursive: true, force: true });
46198
+ const target = path40.join(process.cwd(), ".zelari");
46199
+ await fs22.rm(target, { recursive: true, force: true });
45308
46200
  appendSystem(ctx.setMessages, "[workspace] .zelari/ removed");
45309
46201
  } catch (err) {
45310
46202
  appendSystem(ctx.setMessages, `[workspace reset error] ${err instanceof Error ? err.message : String(err)}`);
@@ -45663,16 +46555,16 @@ function handleModelsRefresh(ctx) {
45663
46555
  }
45664
46556
 
45665
46557
  // src/cli/slashHandlers/skills.ts
45666
- import path40 from "node:path";
46558
+ import path41 from "node:path";
45667
46559
  import os12 from "node:os";
45668
46560
 
45669
46561
  // src/cli/skillHistory.ts
45670
- import { promises as fs22, existsSync as existsSync39, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync4, mkdirSync as mkdirSync17 } from "node:fs";
46562
+ import { promises as fs23, existsSync as existsSync39, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync4, mkdirSync as mkdirSync17 } from "node:fs";
45671
46563
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
45672
46564
  async function readSkillHistory(file2) {
45673
46565
  let raw = "";
45674
46566
  try {
45675
- raw = await fs22.readFile(file2, "utf-8");
46567
+ raw = await fs23.readFile(file2, "utf-8");
45676
46568
  } catch {
45677
46569
  return [];
45678
46570
  }
@@ -45789,7 +46681,7 @@ function handleSkillPicker(ctx, skills, openPicker, fallbackMessage) {
45789
46681
  });
45790
46682
  }
45791
46683
  async function handleSkillStats(ctx, skillId) {
45792
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path40.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
46684
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path41.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
45793
46685
  try {
45794
46686
  const records = await readSkillHistory(historyFile);
45795
46687
  const stats = getSkillStats(records, skillId);
@@ -45805,7 +46697,7 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
45805
46697
  appendSystem(ctx.setMessages, fallbackMessage ?? "[skill-compare] missing args");
45806
46698
  return;
45807
46699
  }
45808
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path40.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
46700
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path41.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
45809
46701
  try {
45810
46702
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
45811
46703
  appendSystem(ctx.setMessages, formatted);
@@ -47530,7 +48422,8 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
47530
48422
  return 1;
47531
48423
  }
47532
48424
  const { planTaskGraph: planTaskGraph2 } = await Promise.resolve().then(() => (init_planner(), planner_exports));
47533
- const { formatKrakenGraphAscii: formatKrakenGraphAscii2 } = await Promise.resolve().then(() => (init_graphStatus(), graphStatus_exports));
48425
+ const { loadGraphSnapshot: loadGraphSnapshot2, formatSnapshotForPlanner: formatSnapshotForPlanner2 } = await Promise.resolve().then(() => (init_graphMemory(), graphMemory_exports));
48426
+ const { formatKrakenGraphAscii: formatKrakenGraphAscii2, formatKrakenGraphDigest: formatKrakenGraphDigest2 } = await Promise.resolve().then(() => (init_graphStatus(), graphStatus_exports));
47534
48427
  const { AuditLogger: AuditLogger2 } = await Promise.resolve().then(() => (init_auditLogger(), auditLogger_exports));
47535
48428
  const { createKrakenSubAgentContextFactory: createKrakenSubAgentContextFactory2 } = await Promise.resolve().then(() => (init_toolRegistry(), toolRegistry_exports));
47536
48429
  const cwd = process.cwd();
@@ -47543,9 +48436,24 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
47543
48436
  `);
47544
48437
  }
47545
48438
  };
48439
+ const abort = new AbortController();
48440
+ const onSigint = () => {
48441
+ log("SIGINT \u2014 cancelling the graph; press Ctrl-C again to force quit");
48442
+ abort.abort();
48443
+ };
48444
+ process.once("SIGINT", onSigint);
47546
48445
  try {
47547
48446
  log(`planning kraken graph: ${prompt}`);
47548
- const graph = await planTaskGraph2({ prompt, provider, model });
48447
+ const previous = await loadGraphSnapshot2(cwd);
48448
+ const previousAttempt = formatSnapshotForPlanner2(previous);
48449
+ if (previousAttempt) log("resuming from the previous unfinished graph");
48450
+ const graph = await planTaskGraph2({
48451
+ prompt,
48452
+ provider,
48453
+ model,
48454
+ cwd,
48455
+ ...previousAttempt ? { previousAttempt } : {}
48456
+ });
47549
48457
  log(formatKrakenGraphAscii2(graph));
47550
48458
  const audit = new AuditLogger2();
47551
48459
  const executor = new KrakenGraphExecutor2({
@@ -47564,10 +48472,21 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
47564
48472
  })
47565
48473
  },
47566
48474
  parentCwd: cwd,
47567
- sessionId
48475
+ sessionId,
48476
+ goal: prompt,
48477
+ signal: abort.signal
47568
48478
  });
47569
48479
  const summary = await executor.execute(graph);
47570
- const finalAscii = formatKrakenGraphAscii2(summary.graph);
48480
+ if (summary.cancelled) log("graph cancelled \u2014 partial results below");
48481
+ const finalAscii = `${formatKrakenGraphAscii2(summary.graph)}
48482
+
48483
+ ${formatKrakenGraphDigest2(
48484
+ summary.graph,
48485
+ {
48486
+ durationsMs: summary.durationsMs,
48487
+ unresolvedFindings: summary.unresolvedFindings
48488
+ }
48489
+ )}`;
47571
48490
  if (opts.output === "json") {
47572
48491
  emitEvent({ type: "message_start" });
47573
48492
  emitEvent({ type: "message_delta", delta: finalAscii });
@@ -47594,6 +48513,8 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
47594
48513
  `);
47595
48514
  }
47596
48515
  return 2;
48516
+ } finally {
48517
+ process.off("SIGINT", onSigint);
47597
48518
  }
47598
48519
  }
47599
48520
  function planModeFromOpts(opts) {
@@ -48709,7 +49630,7 @@ function upsertSkill(opts) {
48709
49630
  }
48710
49631
  dir = getProjectSkillsDir(root);
48711
49632
  }
48712
- const path43 = skillFilePath(dir, name);
49633
+ const path44 = skillFilePath(dir, name);
48713
49634
  const content = serializeSkillMd({
48714
49635
  name,
48715
49636
  description,
@@ -48718,13 +49639,13 @@ function upsertSkill(opts) {
48718
49639
  tools: opts.tools,
48719
49640
  cost: opts.cost
48720
49641
  });
48721
- const parsed = parseSkillMd(content, path43);
49642
+ const parsed = parseSkillMd(content, path44);
48722
49643
  if (!parsed) {
48723
49644
  return { ok: false, error: "Generated SKILL.md failed validation" };
48724
49645
  }
48725
- mkdirSync18(dirname9(path43), { recursive: true });
48726
- writeFileSync20(path43, content, "utf8");
48727
- return { ok: true, path: path43 };
49646
+ mkdirSync18(dirname9(path44), { recursive: true });
49647
+ writeFileSync20(path44, content, "utf8");
49648
+ return { ok: true, path: path44 };
48728
49649
  }
48729
49650
  function removeSkill(opts) {
48730
49651
  const name = opts.name.trim().toLowerCase();
@@ -48742,8 +49663,8 @@ function removeSkill(opts) {
48742
49663
  dir = getProjectSkillsDir(root);
48743
49664
  }
48744
49665
  const skillDir = join34(dir, name);
48745
- const path43 = skillFilePath(dir, name);
48746
- if (!existsSync42(path43) && !existsSync42(skillDir)) {
49666
+ const path44 = skillFilePath(dir, name);
49667
+ if (!existsSync42(path44) && !existsSync42(skillDir)) {
48747
49668
  return { ok: false, error: `Skill "${name}" not found in ${dir}` };
48748
49669
  }
48749
49670
  try {
@@ -48754,7 +49675,7 @@ function removeSkill(opts) {
48754
49675
  error: err instanceof Error ? err.message : String(err)
48755
49676
  };
48756
49677
  }
48757
- return { ok: true, path: path43 };
49678
+ return { ok: true, path: path44 };
48758
49679
  }
48759
49680
 
48760
49681
  // src/cli/generateSkillFromUrl.ts
@@ -48844,8 +49765,8 @@ function normalizeDraft(raw, sourceUrl, provider, model) {
48844
49765
  let name = String(o.name ?? "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
48845
49766
  if (!name || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
48846
49767
  try {
48847
- const path43 = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop()?.replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
48848
- name = path43 && /^[a-z0-9]/.test(path43) ? path43 : "imported-skill";
49768
+ const path44 = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop()?.replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
49769
+ name = path44 && /^[a-z0-9]/.test(path44) ? path44 : "imported-skill";
48849
49770
  } catch {
48850
49771
  name = "imported-skill";
48851
49772
  }