zelari-code 1.48.0 → 1.49.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.
Files changed (59) hide show
  1. package/dist/cli/desktopConfig.js +51 -4
  2. package/dist/cli/desktopConfig.js.map +1 -1
  3. package/dist/cli/hooks/useChatTurn.js +80 -0
  4. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  5. package/dist/cli/kraken/candidateRegistry.js +195 -0
  6. package/dist/cli/kraken/candidateRegistry.js.map +1 -0
  7. package/dist/cli/kraken/candidateRegistry.test.js +211 -0
  8. package/dist/cli/kraken/candidateRegistry.test.js.map +1 -0
  9. package/dist/cli/kraken/completionGate.js +116 -0
  10. package/dist/cli/kraken/completionGate.js.map +1 -0
  11. package/dist/cli/kraken/completionGate.test.js +172 -0
  12. package/dist/cli/kraken/completionGate.test.js.map +1 -0
  13. package/dist/cli/kraken/metrics.js +96 -0
  14. package/dist/cli/kraken/metrics.js.map +1 -0
  15. package/dist/cli/kraken/metrics.test.js +145 -0
  16. package/dist/cli/kraken/metrics.test.js.map +1 -0
  17. package/dist/cli/kraken/selectionPlaybook.js +23 -0
  18. package/dist/cli/kraken/selectionPlaybook.js.map +1 -0
  19. package/dist/cli/kraken/selectionPlaybook.test.js +55 -0
  20. package/dist/cli/kraken/selectionPlaybook.test.js.map +1 -0
  21. package/dist/cli/kraken/turnRuntime.js +207 -0
  22. package/dist/cli/kraken/turnRuntime.js.map +1 -0
  23. package/dist/cli/kraken/turnRuntime.test.js +377 -0
  24. package/dist/cli/kraken/turnRuntime.test.js.map +1 -0
  25. package/dist/cli/kraken/verifier.js +233 -0
  26. package/dist/cli/kraken/verifier.js.map +1 -0
  27. package/dist/cli/kraken/verifier.test.js +232 -0
  28. package/dist/cli/kraken/verifier.test.js.map +1 -0
  29. package/dist/cli/kraken/verifierSettings.test.js +237 -0
  30. package/dist/cli/kraken/verifierSettings.test.js.map +1 -0
  31. package/dist/cli/kraken/verifyReport.js +115 -0
  32. package/dist/cli/kraken/verifyReport.js.map +1 -0
  33. package/dist/cli/kraken/verifyReport.test.js +111 -0
  34. package/dist/cli/kraken/verifyReport.test.js.map +1 -0
  35. package/dist/cli/main.bundled.js +1369 -111
  36. package/dist/cli/main.bundled.js.map +4 -4
  37. package/dist/cli/provider/openai-compatible.js +40 -6
  38. package/dist/cli/provider/openai-compatible.js.map +1 -1
  39. package/dist/cli/provider/openai-compatible.timeout.test.js +72 -0
  40. package/dist/cli/provider/openai-compatible.timeout.test.js.map +1 -0
  41. package/dist/cli/providerConfig.js +46 -0
  42. package/dist/cli/providerConfig.js.map +1 -1
  43. package/dist/cli/runHeadless.js +85 -0
  44. package/dist/cli/runHeadless.js.map +1 -1
  45. package/dist/cli/toolRegistry.js +50 -6
  46. package/dist/cli/toolRegistry.js.map +1 -1
  47. package/dist/cli/tools/krakenSelectTool.js +165 -0
  48. package/dist/cli/tools/krakenSelectTool.js.map +1 -0
  49. package/dist/cli/tools/krakenSelectTool.test.js +246 -0
  50. package/dist/cli/tools/krakenSelectTool.test.js.map +1 -0
  51. package/dist/cli/tools/taskTool.dynamicChecks.test.js +156 -0
  52. package/dist/cli/tools/taskTool.dynamicChecks.test.js.map +1 -0
  53. package/dist/cli/tools/taskTool.js +174 -6
  54. package/dist/cli/tools/taskTool.js.map +1 -1
  55. package/dist/cli/tools/taskTool.planSafety.test.js +280 -0
  56. package/dist/cli/tools/taskTool.planSafety.test.js.map +1 -0
  57. package/dist/cli/tools/taskTool.verifyReport.test.js +140 -0
  58. package/dist/cli/tools/taskTool.verifyReport.test.js.map +1 -0
  59. package/package.json +2 -2
@@ -1473,27 +1473,27 @@ function readStore() {
1473
1473
  }
1474
1474
  return { providers: {} };
1475
1475
  }
1476
- function writeStore(store4) {
1476
+ function writeStore(store6) {
1477
1477
  const file2 = getKeyStorePath();
1478
1478
  mkdirSync2(path3.dirname(file2), { recursive: true });
1479
- writeFileSync2(file2, JSON.stringify(store4, null, 2), { encoding: "utf-8", mode: 384 });
1479
+ writeFileSync2(file2, JSON.stringify(store6, null, 2), { encoding: "utf-8", mode: 384 });
1480
1480
  }
1481
1481
  function setApiKey(providerId, key) {
1482
- const store4 = readStore();
1483
- store4.providers[providerId] = { apiKey: key };
1484
- writeStore(store4);
1482
+ const store6 = readStore();
1483
+ store6.providers[providerId] = { apiKey: key };
1484
+ writeStore(store6);
1485
1485
  }
1486
1486
  function clearApiKey(providerId) {
1487
- const store4 = readStore();
1488
- delete store4.providers[providerId];
1489
- writeStore(store4);
1487
+ const store6 = readStore();
1488
+ delete store6.providers[providerId];
1489
+ writeStore(store6);
1490
1490
  }
1491
1491
  function getStoredApiKey(providerId) {
1492
- const store4 = readStore();
1493
- return store4.providers[providerId]?.apiKey ?? null;
1492
+ const store6 = readStore();
1493
+ return store6.providers[providerId]?.apiKey ?? null;
1494
1494
  }
1495
1495
  function setOAuthToken(providerId, token) {
1496
- const store4 = readStore();
1496
+ const store6 = readStore();
1497
1497
  const entry = { apiKey: token.apiKey };
1498
1498
  if (typeof token.expiresAt === "number" && Number.isFinite(token.expiresAt)) {
1499
1499
  entry.expiresAt = token.expiresAt;
@@ -1507,12 +1507,12 @@ function setOAuthToken(providerId, token) {
1507
1507
  if (typeof token.idToken === "string" && token.idToken.length > 0) {
1508
1508
  entry.idToken = token.idToken;
1509
1509
  }
1510
- store4.providers[providerId] = entry;
1511
- writeStore(store4);
1510
+ store6.providers[providerId] = entry;
1511
+ writeStore(store6);
1512
1512
  }
1513
1513
  function getOAuthToken(providerId) {
1514
- const store4 = readStore();
1515
- return store4.providers[providerId] ?? null;
1514
+ const store6 = readStore();
1515
+ return store6.providers[providerId] ?? null;
1516
1516
  }
1517
1517
  function resolveApiKey(providerId) {
1518
1518
  const spec = getProviderSpec(providerId);
@@ -1571,8 +1571,8 @@ async function forceRefreshOAuth(providerId, options = {}) {
1571
1571
  function readStoreDirect() {
1572
1572
  return readStore();
1573
1573
  }
1574
- function writeStoreDirect(store4) {
1575
- writeStore(store4);
1574
+ function writeStoreDirect(store6) {
1575
+ writeStore(store6);
1576
1576
  }
1577
1577
  function maskKey(key) {
1578
1578
  if (key.length <= 12) return "****";
@@ -1917,9 +1917,11 @@ var init_thinking = __esm({
1917
1917
  var providerConfig_exports = {};
1918
1918
  __export(providerConfig_exports, {
1919
1919
  clearCustomEndpoint: () => clearCustomEndpoint,
1920
+ clearKrakenVerifier: () => clearKrakenVerifier,
1920
1921
  getActiveModel: () => getActiveModel,
1921
1922
  getActiveProvider: () => getActiveProvider,
1922
1923
  getCustomEndpoint: () => getCustomEndpoint,
1924
+ getKrakenVerifierOverride: () => getKrakenVerifierOverride,
1923
1925
  getModelForProvider: () => getModelForProvider,
1924
1926
  getProviderConfig: () => getProviderConfig,
1925
1927
  getProviderConfigPath: () => getProviderConfigPath,
@@ -1927,6 +1929,7 @@ __export(providerConfig_exports, {
1927
1929
  loadProviderConfig: () => loadProviderConfig,
1928
1930
  setActiveProviderId: () => setActiveProviderId,
1929
1931
  setCustomEndpoint: () => setCustomEndpoint,
1932
+ setKrakenVerifier: () => setKrakenVerifier,
1930
1933
  setModelForProvider: () => setModelForProvider,
1931
1934
  setThinkingForProvider: () => setThinkingForProvider
1932
1935
  });
@@ -1950,7 +1953,8 @@ function getProviderConfig() {
1950
1953
  activeProviderId: parsed.activeProviderId,
1951
1954
  modelByProvider: { ...DEFAULTS.modelByProvider, ...parsed.modelByProvider },
1952
1955
  thinkingByProvider: { ...DEFAULTS.thinkingByProvider, ...parsed.thinkingByProvider },
1953
- customEndpoints: mergeCustomEndpoints(parsed.customEndpoints)
1956
+ customEndpoints: mergeCustomEndpoints(parsed.customEndpoints),
1957
+ krakenVerifier: mergeKrakenVerifier(parsed.krakenVerifier)
1954
1958
  };
1955
1959
  }
1956
1960
  } catch {
@@ -2019,6 +2023,35 @@ function clearCustomEndpoint(id) {
2019
2023
  delete config2.customEndpoints[id];
2020
2024
  writeProviderConfig(config2);
2021
2025
  }
2026
+ function mergeKrakenVerifier(raw) {
2027
+ if (!raw || typeof raw !== "object") return void 0;
2028
+ const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
2029
+ const model = typeof raw.model === "string" ? raw.model.trim() : "";
2030
+ if (!provider || !model) return void 0;
2031
+ if (!PROVIDERS.some((p3) => p3.id === provider)) return void 0;
2032
+ return { provider, model };
2033
+ }
2034
+ function getKrakenVerifierOverride() {
2035
+ return getProviderConfig().krakenVerifier;
2036
+ }
2037
+ function setKrakenVerifier(provider, model) {
2038
+ const spec = PROVIDERS.find((p3) => p3.id === provider);
2039
+ if (!spec) {
2040
+ throw new Error(`Unknown provider id: "${provider}". Available: ${PROVIDERS.map((p3) => p3.id).join(", ")}`);
2041
+ }
2042
+ if (!model || model.trim().length === 0) {
2043
+ throw new Error("Verifier model cannot be empty. Use clearKrakenVerifier() to inherit.");
2044
+ }
2045
+ const config2 = getProviderConfig();
2046
+ config2.krakenVerifier = { provider, model: model.trim() };
2047
+ writeProviderConfig(config2);
2048
+ }
2049
+ function clearKrakenVerifier() {
2050
+ const config2 = getProviderConfig();
2051
+ if (!config2.krakenVerifier) return;
2052
+ delete config2.krakenVerifier;
2053
+ writeProviderConfig(config2);
2054
+ }
2022
2055
  function setActiveProviderId(id) {
2023
2056
  const spec = PROVIDERS.find((p3) => p3.id === id);
2024
2057
  if (!spec) {
@@ -20258,7 +20291,7 @@ function getBasePromptModules(mode = "council") {
20258
20291
  function getPromptModule(type) {
20259
20292
  return getBasePromptModules("council").find((m) => m.type === type);
20260
20293
  }
20261
- var CODING_CAPABLE_IDENTITY, COUNCIL_IDENTITY, BEHAVIOR_AGENT, BEHAVIOR_COUNCIL, SAFETY, CONTEXT_SHARING_COUNCIL, OUTPUT_FORMATTING, NATIVE_TOOL_PROTOCOL_MODULE, CODING_PRACTICES_MODULE, TURN_COMPLETION_MODULE, CLARIFICATION_PROTOCOL_MODULE, PROMPT_MODULES, KRAKEN_IDENTITY_MODULE, KRAKEN_LEAD_PLAYBOOK_MODULE, SINGLE_AGENT_IDENTITY_MODULE;
20294
+ var CODING_CAPABLE_IDENTITY, COUNCIL_IDENTITY, BEHAVIOR_AGENT, BEHAVIOR_COUNCIL, SAFETY, CONTEXT_SHARING_COUNCIL, OUTPUT_FORMATTING, NATIVE_TOOL_PROTOCOL_MODULE, CODING_PRACTICES_MODULE, TURN_COMPLETION_MODULE, CLARIFICATION_PROTOCOL_MODULE, PROMPT_MODULES, KRAKEN_IDENTITY_MODULE, KRAKEN_LEAD_PLAYBOOK_MODULE, KRAKEN_SELECTION_PLAYBOOK_MODULE, SINGLE_AGENT_IDENTITY_MODULE;
20262
20295
  var init_promptModules = __esm({
20263
20296
  "packages/core/dist/agents/promptModules.js"() {
20264
20297
  "use strict";
@@ -20456,6 +20489,32 @@ Rules:
20456
20489
  priority: 25,
20457
20490
  content: "# Kraken Lead Playbook (super-agent)\n\nYou are the **parent brain**. Sub-agents spawned with task are tentacles: they cannot see this chat and cannot nest further task calls.\n\n## Default workflow (non-trivial work)\n1. **Orient** - list/read key files; optionally task explore (parallel OK for disjoint questions).\n2. **Decompose** - todo_write with concrete slices and acceptance criteria.\n3. **Implement** - one slice at a time via tools or task agent=general for a bounded unit.\n4. **Verify** - after meaningful writes, run checks yourself (bash / typecheck / tests) or task agent=verify. Do not claim done without on-disk evidence.\n5. **Integrate** - summarize files touched + how to verify; if more remains, checkpoint and ask.\n\n## When to spawn task\n- **explore**: unfamiliar area, multi-file search, map call sites (prefer parallel explores).\n- **general**: isolated implement slice with clear path scope (serial writers unless worktree isolation is on).\n- **verify**: post-implement gate (tests/typecheck/smoke).\n\n## Task contracts (required quality)\nEvery task prompt must be self-contained and include:\n- **Goal** (one sentence)\n- **Scope** (paths / symbols allowed; what is out of scope)\n- **Acceptance** (how the parent will know it succeeded)\n- **Constraints** (no drive-by refactors; match existing style)\n\nOptional tool fields: scope (path allowlist hint), acceptance (checklist). Prefer them when available.\n\n## Caps and discipline\n- Prefer at most 4 explore and 2 general spawns per user turn unless the user asks for more.\n- Do not expand scope beyond the user request.\n- Parallel: many explore OK; general writers stay serial unless ZELARI_KRAKEN_WORKTREE=1.\n- Nested task from children is disabled - you are the only orchestrator.\n- Cheap thoroughness defaults: explore=quick|medium; deep only when stuck.\n- Model routing: explore/verify may use ZELARI_KRAKEN_SUB_MODEL (cheaper); general stays on parent model unless ZELARI_KRAKEN_GENERAL_MODEL is set.\n- After every successful task general, spawn task verify (or run tests yourself) before claiming done - the tool appends a verify-hint footer.\n- Opt-in isolation: ZELARI_KRAKEN_WORKTREE=1 runs general tentacles in a git worktree under .zelari/worktrees/ (KEEP=1 to retain branch for manual merge).\n- Progress bus: tentacle spawns log to .zelari/radio/<session>.jsonl - slash command /kraken shows status.\n\n## Done means verified\nNever end with status theater. Either tools ran and files changed, or you stop with a short report and ask whether to continue."
20458
20491
  };
20492
+ KRAKEN_SELECTION_PLAYBOOK_MODULE = {
20493
+ type: "behavior-rules",
20494
+ title: "Kraken Verified Selection (alpha)",
20495
+ // priority 26 = right after the lead playbook (25); +1000 via custom modules.
20496
+ priority: 26,
20497
+ content: [
20498
+ "# Kraken Verified Selection (alpha)",
20499
+ "",
20500
+ "You can explore competing hypotheses before committing to one implementation path.",
20501
+ "The runtime registers candidates, preserves their evidence verbatim, and judges them via the kraken_select tool.",
20502
+ "",
20503
+ "## When to explore candidates",
20504
+ "- **Simple task** (rename, typo fix, small requested edit, single obvious change): go DIRECT. No candidates, no kraken_select.",
20505
+ "- **Ambiguous task** (two or more plausible root causes or designs): spawn 2 candidates.",
20506
+ "- **High uncertainty** (intermittent bug, race condition, architecture decision with trade-offs): spawn up to 3 candidates.",
20507
+ "",
20508
+ "## Rules",
20509
+ '- Spawn candidates with the task tool using purpose="candidate" - explore-only tentacles; they never write.',
20510
+ "- Each candidate must test a DIFFERENT normalized hypothesis. If two candidates would test the same theory, keep one.",
20511
+ "- Wait for all candidate reports, then call kraken_select exactly once.",
20512
+ "- If the verdict is needs_more_evidence: run at most one more targeted explore, then either re-select or proceed with the best-grounded candidate.",
20513
+ "- Implement ONLY the selected path. Never blend multiple candidates.",
20514
+ "- If the verdict includes required checks: in PLAN fold them into the final plan verification section; in BUILD pass them as the Acceptance criteria of your verify tentacle.",
20515
+ "- A degraded, timed-out, or inconclusive observation is never proof of absence."
20516
+ ].join("\n")
20517
+ };
20459
20518
  SINGLE_AGENT_IDENTITY_MODULE = KRAKEN_IDENTITY_MODULE;
20460
20519
  }
20461
20520
  });
@@ -21014,6 +21073,7 @@ __export(skills_exports, {
21014
21073
  CODING_SKILL_CATALOG: () => CODING_SKILL_CATALOG,
21015
21074
  KRAKEN_IDENTITY_MODULE: () => KRAKEN_IDENTITY_MODULE,
21016
21075
  KRAKEN_LEAD_PLAYBOOK_MODULE: () => KRAKEN_LEAD_PLAYBOOK_MODULE,
21076
+ KRAKEN_SELECTION_PLAYBOOK_MODULE: () => KRAKEN_SELECTION_PLAYBOOK_MODULE,
21017
21077
  LANGUAGE_POLICY_MODULE_TYPE: () => LANGUAGE_POLICY_MODULE_TYPE,
21018
21078
  NATIVE_TOOL_PROTOCOL_MODULE: () => NATIVE_TOOL_PROTOCOL_MODULE,
21019
21079
  SINGLE_AGENT_IDENTITY_MODULE: () => SINGLE_AGENT_IDENTITY_MODULE,
@@ -21260,6 +21320,12 @@ function isBrainTaskSnapshotEvent(e) {
21260
21320
  function isBrainContextMetricsEvent(e) {
21261
21321
  return e.type === "context_metrics";
21262
21322
  }
21323
+ function isBrainKrakenProgressEvent(e) {
21324
+ return e.type === "kraken_progress";
21325
+ }
21326
+ function isBrainKrakenMetricsEvent(e) {
21327
+ return e.type === "kraken_metrics";
21328
+ }
21263
21329
  function createBrainEvent(type, sessionId2, data) {
21264
21330
  return {
21265
21331
  type,
@@ -26972,6 +27038,7 @@ __export(council_exports, {
26972
27038
  IMPLEMENTATION_WRITE_REQUIREMENTS: () => IMPLEMENTATION_WRITE_REQUIREMENTS,
26973
27039
  KRAKEN_IDENTITY_MODULE: () => KRAKEN_IDENTITY_MODULE,
26974
27040
  KRAKEN_LEAD_PLAYBOOK_MODULE: () => KRAKEN_LEAD_PLAYBOOK_MODULE,
27041
+ KRAKEN_SELECTION_PLAYBOOK_MODULE: () => KRAKEN_SELECTION_PLAYBOOK_MODULE,
26975
27042
  LAYOUT_MOTION_PROPS: () => LAYOUT_MOTION_PROPS,
26976
27043
  LESSONS_FILE: () => LESSONS_FILE,
26977
27044
  MAX_DELIVERY_ATTEMPTS: () => MAX_DELIVERY_ATTEMPTS,
@@ -28211,6 +28278,7 @@ __export(dist_exports, {
28211
28278
  IMPLEMENTATION_WRITE_REQUIREMENTS: () => IMPLEMENTATION_WRITE_REQUIREMENTS,
28212
28279
  KRAKEN_IDENTITY_MODULE: () => KRAKEN_IDENTITY_MODULE,
28213
28280
  KRAKEN_LEAD_PLAYBOOK_MODULE: () => KRAKEN_LEAD_PLAYBOOK_MODULE,
28281
+ KRAKEN_SELECTION_PLAYBOOK_MODULE: () => KRAKEN_SELECTION_PLAYBOOK_MODULE,
28214
28282
  LANGUAGE_POLICY_MODULE_TYPE: () => LANGUAGE_POLICY_MODULE_TYPE,
28215
28283
  LAYOUT_MOTION_PROPS: () => LAYOUT_MOTION_PROPS,
28216
28284
  LESSONS_FILE: () => LESSONS_FILE,
@@ -28335,6 +28403,8 @@ __export(dist_exports, {
28335
28403
  isBrainContextMetricsEvent: () => isBrainContextMetricsEvent,
28336
28404
  isBrainCouncilModeEvent: () => isBrainCouncilModeEvent,
28337
28405
  isBrainErrorEvent: () => isBrainErrorEvent,
28406
+ isBrainKrakenMetricsEvent: () => isBrainKrakenMetricsEvent,
28407
+ isBrainKrakenProgressEvent: () => isBrainKrakenProgressEvent,
28338
28408
  isBrainMemberCostEvent: () => isBrainMemberCostEvent,
28339
28409
  isBrainMessageDeltaEvent: () => isBrainMessageDeltaEvent,
28340
28410
  isBrainMessageEndEvent: () => isBrainMessageEndEvent,
@@ -29131,6 +29201,7 @@ __export(openai_compatible_exports, {
29131
29201
  parseCachedPromptTokens: () => parseCachedPromptTokens,
29132
29202
  providerConfigFor: () => providerConfigFor,
29133
29203
  providerFromEnv: () => providerFromEnv,
29204
+ readChunkWithTimeout: () => readChunkWithTimeout,
29134
29205
  resolveActiveProvider: () => resolveActiveProvider,
29135
29206
  resolveBaseUrl: () => resolveBaseUrl
29136
29207
  });
@@ -29156,13 +29227,20 @@ async function readChunkWithTimeout(reader, opts) {
29156
29227
  if (opts.signal?.aborted) {
29157
29228
  throw new Error("aborted");
29158
29229
  }
29159
- const remaining = opts.deadlineMs - Date.now();
29230
+ const now = Date.now();
29231
+ const remaining = opts.deadlineMs - now;
29160
29232
  if (remaining <= 0) {
29161
29233
  throw new Error(
29162
29234
  `Provider stream exceeded max duration (${Math.round(PROVIDER_STREAM_MAX_MS / 1e3)}s). Raise ZELARI_PROVIDER_STREAM_MAX_MS if needed.`
29163
29235
  );
29164
29236
  }
29165
- const waitMs = Math.min(opts.idleMs, remaining);
29237
+ const idleElapsed = now - opts.lastUsefulAt();
29238
+ if (idleElapsed >= opts.idleMs) {
29239
+ throw new Error(
29240
+ `Provider stream idle for ${Math.round(idleElapsed / 1e3)}s (no content tokens \u2014 keep-alive frames don't count). The model/gateway stalled \u2014 try again or switch model. Override with ZELARI_PROVIDER_STREAM_IDLE_MS.`
29241
+ );
29242
+ }
29243
+ const waitMs = Math.min(opts.idleMs - idleElapsed, remaining);
29166
29244
  let idleTimer;
29167
29245
  let onAbort;
29168
29246
  try {
@@ -29449,6 +29527,7 @@ function openaiCompatibleProvider(config2) {
29449
29527
  if (args === null) continue;
29450
29528
  toolCallAccumulator.delete(idx);
29451
29529
  emittedToolCall = true;
29530
+ markUseful();
29452
29531
  yield {
29453
29532
  kind: "tool_call",
29454
29533
  toolCallId: existing.id || `tc-${idx}`,
@@ -29459,6 +29538,10 @@ function openaiCompatibleProvider(config2) {
29459
29538
  toolCallAccumulator.clear();
29460
29539
  };
29461
29540
  const streamDeadline = Date.now() + PROVIDER_STREAM_MAX_MS;
29541
+ let lastUsefulAt = Date.now();
29542
+ const markUseful = () => {
29543
+ lastUsefulAt = Date.now();
29544
+ };
29462
29545
  try {
29463
29546
  while (true) {
29464
29547
  let chunk;
@@ -29466,7 +29549,8 @@ function openaiCompatibleProvider(config2) {
29466
29549
  chunk = await readChunkWithTimeout(reader, {
29467
29550
  idleMs: PROVIDER_STREAM_IDLE_MS,
29468
29551
  deadlineMs: streamDeadline,
29469
- signal: params.signal
29552
+ signal: params.signal,
29553
+ lastUsefulAt: () => lastUsefulAt
29470
29554
  });
29471
29555
  } catch (err) {
29472
29556
  const msg = err instanceof Error ? err.message : String(err);
@@ -29507,6 +29591,7 @@ function openaiCompatibleProvider(config2) {
29507
29591
  const completionTokens = typeof parsed.usage.completion_tokens === "number" ? parsed.usage.completion_tokens : 0;
29508
29592
  const totalTokens = typeof parsed.usage.total_tokens === "number" ? parsed.usage.total_tokens : promptTokens + completionTokens;
29509
29593
  const cachedPromptTokens = parseCachedPromptTokens(parsed.usage);
29594
+ markUseful();
29510
29595
  yield {
29511
29596
  kind: "usage",
29512
29597
  usage: {
@@ -29518,10 +29603,12 @@ function openaiCompatibleProvider(config2) {
29518
29603
  };
29519
29604
  }
29520
29605
  if (typeof delta?.content === "string" && delta.content.length > 0) {
29606
+ markUseful();
29521
29607
  yield { kind: "text", delta: delta.content };
29522
29608
  }
29523
29609
  const reasoning = delta?.reasoning_content ?? delta?.reasoning;
29524
29610
  if (typeof reasoning === "string" && reasoning.length > 0) {
29611
+ markUseful();
29525
29612
  yield { kind: "thinking", delta: reasoning };
29526
29613
  }
29527
29614
  const details = delta?.reasoning_details;
@@ -29533,9 +29620,13 @@ function openaiCompatibleProvider(config2) {
29533
29620
  if (t.startsWith(reasoningDetailsBuf)) {
29534
29621
  const piece = t.slice(reasoningDetailsBuf.length);
29535
29622
  reasoningDetailsBuf = t;
29536
- if (piece.length > 0) yield { kind: "thinking", delta: piece };
29623
+ if (piece.length > 0) {
29624
+ markUseful();
29625
+ yield { kind: "thinking", delta: piece };
29626
+ }
29537
29627
  } else {
29538
29628
  reasoningDetailsBuf += t;
29629
+ markUseful();
29539
29630
  yield { kind: "thinking", delta: t };
29540
29631
  }
29541
29632
  }
@@ -29556,6 +29647,7 @@ function openaiCompatibleProvider(config2) {
29556
29647
  }
29557
29648
  if (choice?.finish_reason) {
29558
29649
  yield* flushToolAccumulator();
29650
+ markUseful();
29559
29651
  const reason = choice.finish_reason === "stop" && emittedToolCall ? "tool_calls" : choice.finish_reason;
29560
29652
  yield { kind: "finish", reason };
29561
29653
  }
@@ -31133,6 +31225,362 @@ var init_krakenWorktree = __esm({
31133
31225
  }
31134
31226
  });
31135
31227
 
31228
+ // src/cli/kraken/candidateRegistry.ts
31229
+ function isKrakenSelectionEnabled() {
31230
+ return process.env.ZELARI_KRAKEN_SELECTION === "1";
31231
+ }
31232
+ function store2() {
31233
+ const g = globalThis;
31234
+ if (!g.__zelariKrakenCandidates) g.__zelariKrakenCandidates = [];
31235
+ return g.__zelariKrakenCandidates;
31236
+ }
31237
+ function resetKrakenCandidates() {
31238
+ const g = globalThis;
31239
+ g.__zelariKrakenCandidates = [];
31240
+ g.__zelariKrakenSelection = null;
31241
+ g.__zelariKrakenCheckResults = null;
31242
+ }
31243
+ function setKrakenSelection(verdict) {
31244
+ const g = globalThis;
31245
+ g.__zelariKrakenSelection = verdict;
31246
+ }
31247
+ function getKrakenSelection() {
31248
+ const g = globalThis;
31249
+ return g.__zelariKrakenSelection ?? null;
31250
+ }
31251
+ function krakenRequiredChecks() {
31252
+ const verdict = getKrakenSelection();
31253
+ if (!verdict || verdict.status !== "selected") return [];
31254
+ return verdict.requiredChecks;
31255
+ }
31256
+ function setKrakenCheckResults(results) {
31257
+ const g = globalThis;
31258
+ g.__zelariKrakenCheckResults = results;
31259
+ }
31260
+ function getKrakenCheckResults() {
31261
+ const g = globalThis;
31262
+ const results = g.__zelariKrakenCheckResults;
31263
+ return results ? [...results] : null;
31264
+ }
31265
+ function krakenChecksPassed() {
31266
+ const results = getKrakenCheckResults();
31267
+ if (!results) return void 0;
31268
+ return results.filter((r) => r.status === "pass").length;
31269
+ }
31270
+ function krakenCandidates() {
31271
+ return [...store2()];
31272
+ }
31273
+ function reserveCandidateSlot() {
31274
+ const n = store2().length;
31275
+ if (n >= KRAKEN_CANDIDATE_CAP) {
31276
+ return {
31277
+ error: `task: candidate cap reached (${KRAKEN_CANDIDATE_CAP}). Compare the existing candidates instead of spawning more.`
31278
+ };
31279
+ }
31280
+ return { index: n + 1 };
31281
+ }
31282
+ function parseCandidateReport(raw) {
31283
+ const open = raw.lastIndexOf(REPORT_OPEN);
31284
+ const close = raw.lastIndexOf(REPORT_CLOSE);
31285
+ if (open === -1 || close === -1 || close < open) {
31286
+ return { ok: false, error: "missing report block" };
31287
+ }
31288
+ const body = raw.slice(open + REPORT_OPEN.length, close).trim();
31289
+ let parsed;
31290
+ try {
31291
+ parsed = JSON.parse(body);
31292
+ } catch (err) {
31293
+ return {
31294
+ ok: false,
31295
+ error: `invalid JSON (${err instanceof Error ? err.message : String(err)})`
31296
+ };
31297
+ }
31298
+ if (typeof parsed !== "object" || parsed === null) {
31299
+ return { ok: false, error: "report is not a JSON object" };
31300
+ }
31301
+ const obj = parsed;
31302
+ const evidence = Array.isArray(obj.evidence) ? obj.evidence.filter((e) => typeof e === "object" && e !== null).map((e) => ({
31303
+ claim: typeof e.claim === "string" ? e.claim : "",
31304
+ basis: typeof e.basis === "string" ? e.basis : "",
31305
+ degraded: e.degraded === true
31306
+ })).filter((e) => e.claim.trim().length > 0) : [];
31307
+ const risks = Array.isArray(obj.risks) ? obj.risks.filter((r) => typeof r === "string" && r.trim().length > 0) : [];
31308
+ return {
31309
+ ok: true,
31310
+ report: {
31311
+ hypothesis: typeof obj.hypothesis === "string" ? obj.hypothesis.trim() : "",
31312
+ evidence,
31313
+ risks,
31314
+ hasDegradedEvidence: evidence.some((e) => e.degraded)
31315
+ }
31316
+ };
31317
+ }
31318
+ function registerCandidate(entry) {
31319
+ const withIndex = { ...entry, index: entry.index ?? store2().length + 1 };
31320
+ store2().push(withIndex);
31321
+ return withIndex;
31322
+ }
31323
+ function candidateInstructions(index, cap3 = KRAKEN_CANDIDATE_CAP) {
31324
+ return CANDIDATE_INSTRUCTIONS.replaceAll("{index}", String(index)).replaceAll("{cap}", String(cap3));
31325
+ }
31326
+ var KRAKEN_CANDIDATE_CAP, REPORT_OPEN, REPORT_CLOSE, CANDIDATE_INSTRUCTIONS;
31327
+ var init_candidateRegistry = __esm({
31328
+ "src/cli/kraken/candidateRegistry.ts"() {
31329
+ "use strict";
31330
+ KRAKEN_CANDIDATE_CAP = 3;
31331
+ REPORT_OPEN = "<candidate-report>";
31332
+ REPORT_CLOSE = "</candidate-report>";
31333
+ CANDIDATE_INSTRUCTIONS = [
31334
+ "You are CANDIDATE #{index} of at most {cap}: one independent hypothesis",
31335
+ "about the task, researched in parallel with other candidates.",
31336
+ "DIVERSITY: focus on ONE hypothesis and pursue it honestly \u2014 do not try to",
31337
+ "cover every angle. A narrower, well-evidenced hypothesis beats a broad guess.",
31338
+ "OBSERVATION INTEGRITY: mark degraded or inconclusive observations explicitly",
31339
+ "(timeout, zero files walked, unavailable backend) \u2014 degraded is NOT proof",
31340
+ "of absence.",
31341
+ "END your final message with EXACTLY this block (valid JSON, no prose after):",
31342
+ "<candidate-report>",
31343
+ "{",
31344
+ ' "hypothesis": "one-sentence hypothesis you investigated",',
31345
+ ' "evidence": [',
31346
+ ' { "claim": "what you found", "basis": "tool + file:line or command", "degraded": false }',
31347
+ " ],",
31348
+ ' "risks": ["open questions / weaknesses"]',
31349
+ "}",
31350
+ "</candidate-report>"
31351
+ ].join("\n");
31352
+ }
31353
+ });
31354
+
31355
+ // src/cli/kraken/verifyReport.ts
31356
+ function normalize(text) {
31357
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
31358
+ }
31359
+ function extractVerifyReportBlocks(raw) {
31360
+ const blocks = [];
31361
+ let cursor = 0;
31362
+ for (; ; ) {
31363
+ const open = raw.indexOf(OPEN, cursor);
31364
+ if (open === -1) break;
31365
+ const close = raw.indexOf(CLOSE, open + OPEN.length);
31366
+ if (close === -1) break;
31367
+ const block = { check: "", status: "" };
31368
+ for (const line of raw.slice(open + OPEN.length, close).split(/\r?\n/)) {
31369
+ const m = /^(check|status|note)\s*:\s*(.*)$/i.exec(line.trim());
31370
+ if (!m) continue;
31371
+ const value = m[2].trim();
31372
+ const key = m[1].toLowerCase();
31373
+ if (key === "check" && !block.check) block.check = value;
31374
+ else if (key === "status" && !block.status) block.status = value;
31375
+ else if (key === "note" && block.note === void 0) block.note = value;
31376
+ }
31377
+ if (block.check) blocks.push(block);
31378
+ cursor = close + CLOSE.length;
31379
+ }
31380
+ return blocks;
31381
+ }
31382
+ function parseVerifyReport(raw, requiredChecks) {
31383
+ const byCriterion = /* @__PURE__ */ new Map();
31384
+ for (const block of extractVerifyReportBlocks(raw)) {
31385
+ byCriterion.set(normalize(block.check), block);
31386
+ }
31387
+ const keys = [...byCriterion.keys()];
31388
+ return requiredChecks.map((check2) => {
31389
+ const norm = normalize(check2);
31390
+ let block = byCriterion.get(norm) ?? null;
31391
+ if (!block) {
31392
+ for (const key of keys) {
31393
+ if (key.length >= 8 && (key.includes(norm) || norm.includes(key))) {
31394
+ block = byCriterion.get(key) ?? null;
31395
+ break;
31396
+ }
31397
+ }
31398
+ }
31399
+ if (!block) {
31400
+ return {
31401
+ check: check2,
31402
+ status: "unknown",
31403
+ note: "no verify-report block for this check"
31404
+ };
31405
+ }
31406
+ const status = VALID_STATUSES.has(block.status) ? block.status : "unknown";
31407
+ return { check: check2, status, ...block.note ? { note: block.note } : {} };
31408
+ });
31409
+ }
31410
+ function allUnknownCheckResults(requiredChecks, reason) {
31411
+ return requiredChecks.map((check2) => ({
31412
+ check: check2,
31413
+ status: "unknown",
31414
+ note: reason
31415
+ }));
31416
+ }
31417
+ var OPEN, CLOSE, VALID_STATUSES;
31418
+ var init_verifyReport = __esm({
31419
+ "src/cli/kraken/verifyReport.ts"() {
31420
+ "use strict";
31421
+ OPEN = "<verify-report>";
31422
+ CLOSE = "</verify-report>";
31423
+ VALID_STATUSES = /* @__PURE__ */ new Set(["pass", "fail", "unknown"]);
31424
+ }
31425
+ });
31426
+
31427
+ // src/cli/kraken/completionGate.ts
31428
+ function normalize2(text) {
31429
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
31430
+ }
31431
+ function classifyKrakenChecks(requiredChecks, results) {
31432
+ const byCriterion = /* @__PURE__ */ new Map();
31433
+ for (const result of results ?? []) {
31434
+ byCriterion.set(normalize2(result.check), result);
31435
+ }
31436
+ const keys = [...byCriterion.keys()];
31437
+ const out = { passed: [], failed: [], unknown: [] };
31438
+ for (const check2 of requiredChecks) {
31439
+ const norm = normalize2(check2);
31440
+ let result = byCriterion.get(norm) ?? null;
31441
+ if (!result) {
31442
+ for (const key of keys) {
31443
+ if (key.length >= 8 && (key.includes(norm) || norm.includes(key))) {
31444
+ result = byCriterion.get(key) ?? null;
31445
+ break;
31446
+ }
31447
+ }
31448
+ }
31449
+ if (!result || result.status === "unknown") out.unknown.push(check2);
31450
+ else if (result.status === "pass") out.passed.push(check2);
31451
+ else out.failed.push(check2);
31452
+ }
31453
+ return out;
31454
+ }
31455
+ function evaluateKrakenCompletionGate(mode) {
31456
+ try {
31457
+ if (mode !== "build") return OPEN_GATE;
31458
+ const checks = krakenRequiredChecks();
31459
+ if (checks.length === 0) return OPEN_GATE;
31460
+ const classification = classifyKrakenChecks(checks, getKrakenCheckResults());
31461
+ return {
31462
+ blocked: classification.failed.length > 0 || classification.unknown.length > 0,
31463
+ selectionUsed: true,
31464
+ total: checks.length,
31465
+ passed: classification.passed.length,
31466
+ failedChecks: classification.failed,
31467
+ unknownChecks: classification.unknown
31468
+ };
31469
+ } catch {
31470
+ return OPEN_GATE;
31471
+ }
31472
+ }
31473
+ function buildKrakenRepairPrompt(gate) {
31474
+ const lines = [
31475
+ `The BUILD turn is ending, but the required checks from kraken_select are not all satisfied (passed ${gate.passed}/${gate.total}).`,
31476
+ ""
31477
+ ];
31478
+ if (gate.failedChecks.length > 0) {
31479
+ lines.push("FAILED checks (evidence contradicts them):");
31480
+ for (const check2 of gate.failedChecks) lines.push(`- ${check2}`);
31481
+ lines.push("");
31482
+ }
31483
+ if (gate.unknownChecks.length > 0) {
31484
+ lines.push(
31485
+ "UNKNOWN checks (never conclusively verified \u2014 a degraded or missing observation is NOT proof):"
31486
+ );
31487
+ for (const check2 of gate.unknownChecks) lines.push(`- ${check2}`);
31488
+ lines.push("");
31489
+ }
31490
+ lines.push(
31491
+ "Recover this turn:",
31492
+ "1. The approach selection is settled \u2014 do NOT call kraken_select again.",
31493
+ "2. Fix each failing check directly with focused edits.",
31494
+ "3. Make each unknown check conclusively verifiable (run the real command, read the real output).",
31495
+ "4. Spawn a `task verify` tentacle whose Acceptance lists ALL required checks; its conclusion must contain one <verify-report> block per check with an explicit status.",
31496
+ "5. Only end the turn when every check reports status: pass \u2014 an unverified assumption is not a pass."
31497
+ );
31498
+ return lines.join("\n");
31499
+ }
31500
+ var OPEN_GATE;
31501
+ var init_completionGate = __esm({
31502
+ "src/cli/kraken/completionGate.ts"() {
31503
+ "use strict";
31504
+ init_candidateRegistry();
31505
+ OPEN_GATE = {
31506
+ blocked: false,
31507
+ selectionUsed: false,
31508
+ total: 0,
31509
+ passed: 0,
31510
+ failedChecks: [],
31511
+ unknownChecks: []
31512
+ };
31513
+ }
31514
+ });
31515
+
31516
+ // src/cli/kraken/metrics.ts
31517
+ function store3() {
31518
+ const g = globalThis;
31519
+ g.__zelariKrakenTurnMetrics ??= {
31520
+ candidateTokens: 0,
31521
+ selectionRecorded: false,
31522
+ selectionFallback: false,
31523
+ repairTriggered: false,
31524
+ repairSucceeded: false
31525
+ };
31526
+ return g.__zelariKrakenTurnMetrics;
31527
+ }
31528
+ function resetKrakenTurnMetrics() {
31529
+ const g = globalThis;
31530
+ g.__zelariKrakenTurnMetrics = void 0;
31531
+ }
31532
+ function recordCandidateTokens(totalTokens) {
31533
+ if (!Number.isFinite(totalTokens) || totalTokens <= 0) return;
31534
+ store3().candidateTokens += Math.round(totalTokens);
31535
+ }
31536
+ function recordSelectionOutcome(outcome) {
31537
+ const s = store3();
31538
+ s.selectionRecorded = true;
31539
+ s.selectionLatencyMs = Math.max(0, Math.round(outcome.latencyMs));
31540
+ if (typeof outcome.tokens === "number" && Number.isFinite(outcome.tokens) && outcome.tokens > 0) {
31541
+ s.selectionTokens = Math.round(outcome.tokens);
31542
+ }
31543
+ s.selectionFallback = outcome.degraded;
31544
+ s.selectionFallbackReason = outcome.fallbackReason;
31545
+ }
31546
+ function markRepairTriggered() {
31547
+ store3().repairTriggered = true;
31548
+ }
31549
+ function markRepairSucceeded() {
31550
+ if (store3().repairTriggered) store3().repairSucceeded = true;
31551
+ }
31552
+ function collectKrakenTurnMetrics() {
31553
+ const s = store3();
31554
+ const verdict = getKrakenSelection();
31555
+ const candidates = krakenCandidates();
31556
+ if (!verdict && candidates.length === 0 && !s.selectionRecorded && !s.repairTriggered) {
31557
+ return null;
31558
+ }
31559
+ const classification = classifyKrakenChecks(krakenRequiredChecks(), getKrakenCheckResults());
31560
+ return {
31561
+ selectionUsed: verdict !== null,
31562
+ candidateCount: candidates.length,
31563
+ candidateTokens: s.candidateTokens,
31564
+ ...s.selectionTokens !== void 0 ? { selectionTokens: s.selectionTokens } : {},
31565
+ ...s.selectionLatencyMs !== void 0 ? { selectionLatencyMs: s.selectionLatencyMs } : {},
31566
+ selectionFallback: s.selectionFallback,
31567
+ ...s.selectionFallbackReason !== void 0 ? { selectionFallbackReason: s.selectionFallbackReason } : {},
31568
+ needsMoreEvidence: verdict?.status === "needs_more_evidence",
31569
+ verificationPass: classification.passed.length,
31570
+ verificationFail: classification.failed.length,
31571
+ verificationUnknown: classification.unknown.length,
31572
+ repairTriggered: s.repairTriggered,
31573
+ repairSucceeded: s.repairSucceeded
31574
+ };
31575
+ }
31576
+ var init_metrics2 = __esm({
31577
+ "src/cli/kraken/metrics.ts"() {
31578
+ "use strict";
31579
+ init_candidateRegistry();
31580
+ init_completionGate();
31581
+ }
31582
+ });
31583
+
31136
31584
  // src/cli/tools/taskTool.ts
31137
31585
  function resetTaskSpawnCount() {
31138
31586
  const g = globalThis;
@@ -31162,6 +31610,21 @@ function buildTaskUserPrompt(args) {
31162
31610
  }
31163
31611
  return parts.join("\n");
31164
31612
  }
31613
+ function withKrakenRequiredChecks(agent, acceptance) {
31614
+ if (agent !== "verify") return acceptance;
31615
+ const required2 = krakenRequiredChecks();
31616
+ if (required2.length === 0) return acceptance;
31617
+ const seen = new Set((acceptance ?? []).map((a) => a.trim().toLowerCase()));
31618
+ const merged = [...acceptance ?? []];
31619
+ for (const check2 of required2) {
31620
+ const key = check2.trim().toLowerCase();
31621
+ if (!seen.has(key)) {
31622
+ seen.add(key);
31623
+ merged.push(check2);
31624
+ }
31625
+ }
31626
+ return merged;
31627
+ }
31165
31628
  function systemPromptForAgent(agent) {
31166
31629
  if (agent === "general") return GENERAL_PROMPT;
31167
31630
  if (agent === "verify") return VERIFY_PROMPT;
@@ -31187,6 +31650,7 @@ async function runSubAgent(harness, opts = {}) {
31187
31650
  let current = "";
31188
31651
  let lastCompleted = "";
31189
31652
  let error51;
31653
+ let usage;
31190
31654
  if (signal?.aborted) return { result: "", aborted: true };
31191
31655
  for await (const ev of harness.run()) {
31192
31656
  if (signal?.aborted) {
@@ -31205,6 +31669,16 @@ async function runSubAgent(harness, opts = {}) {
31205
31669
  break;
31206
31670
  case "message_end":
31207
31671
  if (current.trim()) lastCompleted = current;
31672
+ if (ev.usage) {
31673
+ usage = usage ? {
31674
+ promptTokens: usage.promptTokens + ev.usage.promptTokens,
31675
+ completionTokens: usage.completionTokens + ev.usage.completionTokens,
31676
+ totalTokens: usage.totalTokens + ev.usage.totalTokens,
31677
+ ...(usage.cachedPromptTokens ?? 0) + (ev.usage.cachedPromptTokens ?? 0) > 0 ? {
31678
+ cachedPromptTokens: (usage.cachedPromptTokens ?? 0) + (ev.usage.cachedPromptTokens ?? 0)
31679
+ } : {}
31680
+ } : ev.usage;
31681
+ }
31208
31682
  current = "";
31209
31683
  break;
31210
31684
  case "error":
@@ -31215,7 +31689,7 @@ async function runSubAgent(harness, opts = {}) {
31215
31689
  }
31216
31690
  }
31217
31691
  const result = (lastCompleted || current).trim();
31218
- return { result, ...error51 ? { error: error51 } : {} };
31692
+ return { result, ...error51 ? { error: error51 } : {}, ...usage ? { usage } : {} };
31219
31693
  }
31220
31694
  async function runTentacle(opts) {
31221
31695
  const { deps, args, agent, thoroughness, parentCwd, sessionId: sessionId2 } = opts;
@@ -31290,7 +31764,7 @@ async function runTentacle(opts) {
31290
31764
  const userContent = buildTaskUserPrompt({
31291
31765
  prompt: args.prompt,
31292
31766
  scope: args.scope,
31293
- acceptance: args.acceptance
31767
+ acceptance: withKrakenRequiredChecks(agent, args.acceptance)
31294
31768
  });
31295
31769
  const maxToolCalls = maxToolCallsForThoroughness(thoroughness, agent);
31296
31770
  const runCwd = sub.cwd || effectiveCwd;
@@ -31325,7 +31799,7 @@ async function runTentacle(opts) {
31325
31799
  error: `task: failed to start sub-agent \u2014 ${err instanceof Error ? err.message : String(err)}`
31326
31800
  };
31327
31801
  }
31328
- const { result, error: error51, aborted: aborted2 } = await runSubAgent(harness, {
31802
+ const { result, error: error51, aborted: aborted2, usage } = await runSubAgent(harness, {
31329
31803
  ...opts.signal ? { signal: opts.signal } : {}
31330
31804
  });
31331
31805
  const durationMs = Date.now() - started;
@@ -31426,19 +31900,50 @@ ${verifyHintForGeneral(args.acceptance)}`;
31426
31900
  model: sub.model,
31427
31901
  result,
31428
31902
  footer,
31903
+ ...usage ? { usage } : {},
31429
31904
  worktreePath: worktree?.path ?? null,
31430
31905
  worktreeHandle: worktree
31431
31906
  };
31432
31907
  }
31433
- function createTaskTool(deps) {
31908
+ function createTaskTool(deps, policy = {}) {
31909
+ const allowedAgents = policy.allowedAgents ?? ["explore", "general", "verify"];
31910
+ const restricted = policy.allowedAgents !== void 0 && !(policy.allowedAgents.includes("explore") && policy.allowedAgents.includes("general") && policy.allowedAgents.includes("verify"));
31911
+ const inputSchema2 = restricted ? TaskArgsSchema.extend({
31912
+ agent: external_exports.enum(allowedAgents).optional().describe(
31913
+ `Sub-agent type. In this mode ONLY ${allowedAgents.join("|")} is allowed (plan-safe read-only tentacles).`
31914
+ )
31915
+ }) : TaskArgsSchema;
31434
31916
  return {
31435
31917
  name: "task",
31436
- description: "Delegate a focused sub-task to an isolated sub-agent with its own context; returns only a concise conclusion (keeps parent context lean).\n- agent=explore (default): read-only research/search\n- agent=general: can edit files for one bounded unit of work\n- agent=verify: read + bash to run tests/checks\nProvide a fully self-contained `prompt` (sub-agent cannot see this conversation). Optional scope[] + acceptance[] contracts. After general, follow up with verify.",
31918
+ description: "Delegate a focused sub-task to an isolated sub-agent with its own context; returns only a concise conclusion (keeps parent context lean).\n- agent=explore (default): read-only research/search\n- agent=general: can edit files for one bounded unit of work\n- agent=verify: read + bash to run tests/checks\nProvide a fully self-contained `prompt` (sub-agent cannot see this conversation). Optional scope[] + acceptance[] contracts. After general, follow up with verify." + (restricted ? `
31919
+ RESTRICTED in this mode: only agent=${allowedAgents.join("|")} is allowed.` : ""),
31437
31920
  permissions: ["read", "network", "write", "execute"],
31438
31921
  timeoutMs: 3e5,
31439
- inputSchema: TaskArgsSchema,
31922
+ inputSchema: inputSchema2,
31440
31923
  execute: async (args, ctx) => {
31441
31924
  const agent = args.agent ?? "explore";
31925
+ let candidateSlot = 0;
31926
+ if (!allowedAgents.includes(agent)) {
31927
+ return typedErr(
31928
+ `task: agent=${agent} is not allowed in this mode. Allowed: ${allowedAgents.join(", ")} (plan-safe read-only tentacles). Re-issue with an allowed agent kind.`
31929
+ );
31930
+ }
31931
+ const isCandidate = args.purpose === "candidate";
31932
+ if (isCandidate) {
31933
+ if (!isKrakenSelectionEnabled()) {
31934
+ return typedErr(
31935
+ "task: purpose=candidate requires ZELARI_KRAKEN_SELECTION=1 (alpha feature). Spawn a plain explore tentacle instead."
31936
+ );
31937
+ }
31938
+ if (agent !== "explore") {
31939
+ return typedErr(
31940
+ "task: purpose=candidate forces agent=explore (candidates are read-only in v1 \u2014 zero candidate implementations, ADR-0020)."
31941
+ );
31942
+ }
31943
+ const slot = reserveCandidateSlot();
31944
+ if ("error" in slot) return typedErr(slot.error);
31945
+ candidateSlot = slot.index;
31946
+ }
31442
31947
  const thoroughness = args.thoroughness ?? "medium";
31443
31948
  const sessionId2 = ctx.sessionId || "default";
31444
31949
  const parentCwd = ctx.cwd || process.cwd();
@@ -31461,8 +31966,51 @@ function createTaskTool(deps) {
31461
31966
  agent,
31462
31967
  thoroughness,
31463
31968
  parentCwd,
31464
- sessionId: sessionId2
31969
+ sessionId: sessionId2,
31970
+ // Fase 1 (ADR-0020): propagate the parent turn's cancellation signal
31971
+ // so cancel/timeout unwinds the tentacle instead of letting it run on.
31972
+ ...ctx.signal ? { signal: ctx.signal } : {},
31973
+ ...isCandidate ? {
31974
+ systemPromptOverride: systemPromptForAgent("explore") + "\n\n" + candidateInstructions(candidateSlot)
31975
+ } : {}
31465
31976
  });
31977
+ if (isCandidate) {
31978
+ recordCandidateTokens(res.ok ? res.usage?.totalTokens ?? 0 : 0);
31979
+ if (res.ok) {
31980
+ const parsed = parseCandidateReport(res.result);
31981
+ registerCandidate(
31982
+ parsed.ok ? {
31983
+ status: "ok",
31984
+ index: candidateSlot,
31985
+ description: args.description,
31986
+ report: parsed.report,
31987
+ raw: res.result
31988
+ } : {
31989
+ status: "malformed",
31990
+ index: candidateSlot,
31991
+ description: args.description,
31992
+ error: parsed.error,
31993
+ raw: res.result
31994
+ }
31995
+ );
31996
+ } else {
31997
+ registerCandidate({
31998
+ status: "malformed",
31999
+ index: candidateSlot,
32000
+ description: args.description,
32001
+ error: res.error,
32002
+ raw: ""
32003
+ });
32004
+ }
32005
+ }
32006
+ if (agent === "verify") {
32007
+ const required2 = krakenRequiredChecks();
32008
+ if (required2.length > 0) {
32009
+ setKrakenCheckResults(
32010
+ res.ok ? parseVerifyReport(res.result, required2) : allUnknownCheckResults(required2, `verify tentacle failed: ${res.error}`)
32011
+ );
32012
+ }
32013
+ }
31466
32014
  if (!res.ok) return typedErr(res.error);
31467
32015
  return typedOk({
31468
32016
  result: `[sub-agent:${res.agent}/${res.thoroughness} model=${res.model}]
@@ -31472,7 +32020,7 @@ ${res.result}${res.footer}`,
31472
32020
  }
31473
32021
  };
31474
32022
  }
31475
- var EXPLORE_PROMPT, GENERAL_PROMPT, VERIFY_PROMPT, TaskArgsSchema;
32023
+ var EXPLORE_PROMPT, GENERAL_PROMPT, VERIFY_PROMPT, TaskArgsSchema, TaskPurposeSchema, TaskArgsWithPurposeSchema;
31476
32024
  var init_taskTool = __esm({
31477
32025
  "src/cli/tools/taskTool.ts"() {
31478
32026
  "use strict";
@@ -31481,6 +32029,9 @@ var init_taskTool = __esm({
31481
32029
  init_krakenRadio();
31482
32030
  init_krakenWorktree();
31483
32031
  init_krakenLive();
32032
+ init_candidateRegistry();
32033
+ init_verifyReport();
32034
+ init_metrics2();
31484
32035
  EXPLORE_PROMPT = [
31485
32036
  "You are a focused EXPLORE tentacle of Kraken (parent super-agent).",
31486
32037
  "READ-ONLY tools only (read, list, grep, fetch). No edits, no shell.",
@@ -31506,7 +32057,16 @@ var init_taskTool = __esm({
31506
32057
  "You may read files and run test/build commands via bash. Prefer",
31507
32058
  "targeted checks over full suite when possible.",
31508
32059
  "Report: pass/fail, commands run, key output, and gaps vs Acceptance criteria.",
31509
- "If Acceptance criteria are listed, check each one explicitly."
32060
+ "If Acceptance criteria are listed, check each one explicitly.",
32061
+ "End your final message with ONE <verify-report> block per acceptance",
32062
+ "criterion (required checks included), in this exact shape:",
32063
+ "<verify-report>",
32064
+ "check: <criterion text as given>",
32065
+ "status: pass | fail | unknown",
32066
+ "note: <one line of evidence (command + outcome)>",
32067
+ "</verify-report>",
32068
+ "Use status=unknown when you could NOT determine the outcome (degraded",
32069
+ "tool, timeout, inconclusive evidence) \u2014 never guess pass."
31510
32070
  ].join("\n");
31511
32071
  TaskArgsSchema = external_exports.object({
31512
32072
  description: external_exports.string().min(1).describe("A 3-6 word label for the sub-task (for logs/UI)."),
@@ -31524,6 +32084,336 @@ var init_taskTool = __esm({
31524
32084
  "Optional acceptance checklist (contract). Appended to the prompt as Acceptance criteria."
31525
32085
  )
31526
32086
  });
32087
+ TaskPurposeSchema = external_exports.enum(["candidate"]).optional().describe(
32088
+ "Mark this explore tentacle as one CANDIDATE hypothesis (alpha: requires ZELARI_KRAKEN_SELECTION=1). Forces agent=explore, structured report."
32089
+ );
32090
+ TaskArgsWithPurposeSchema = TaskArgsSchema.extend({
32091
+ purpose: TaskPurposeSchema
32092
+ });
32093
+ }
32094
+ });
32095
+
32096
+ // src/cli/kraken/verifier.ts
32097
+ function resolveKrakenVerifier(parent, env = process.env, explicit) {
32098
+ if (explicit?.provider?.trim() && explicit?.model?.trim()) {
32099
+ return { provider: explicit.provider.trim(), model: explicit.model.trim() };
32100
+ }
32101
+ const envProvider = env.ZELARI_KRAKEN_SELECT_PROVIDER?.trim();
32102
+ const envModel = env.ZELARI_KRAKEN_SELECT_MODEL?.trim();
32103
+ if (envProvider && envModel) return { provider: envProvider, model: envModel };
32104
+ if (envModel) return { provider: parent.provider, model: envModel };
32105
+ return { provider: parent.provider, model: parent.model };
32106
+ }
32107
+ function renderCandidate(entry) {
32108
+ if (entry.status === "malformed") {
32109
+ return [
32110
+ `## Candidate #${entry.index} \u2014 UNUSABLE (malformed report: ${entry.error})`,
32111
+ "This candidate cannot win; it is listed for completeness."
32112
+ ].join("\n");
32113
+ }
32114
+ const lines = [`## Candidate #${entry.index} \u2014 ${entry.report.hypothesis || "(no hypothesis stated)"}`];
32115
+ if (entry.report.evidence.length === 0) {
32116
+ lines.push("Evidence: NONE (unsupported hypothesis)");
32117
+ } else {
32118
+ lines.push("Evidence:");
32119
+ for (const e of entry.report.evidence) {
32120
+ const tag = e.degraded ? "DEGRADED \u2014 inconclusive, NOT proof of absence" : "OK";
32121
+ lines.push(`- [${tag}] ${e.claim}${e.basis ? ` (basis: ${e.basis})` : ""}`);
32122
+ }
32123
+ }
32124
+ if (entry.report.risks.length > 0) {
32125
+ lines.push("Risks:");
32126
+ for (const r of entry.report.risks) lines.push(`- ${r}`);
32127
+ }
32128
+ return lines.join("\n");
32129
+ }
32130
+ function buildSelectionPrompt(task, candidates) {
32131
+ return [
32132
+ "TASK",
32133
+ task || "(the current user task \u2014 judge against the candidates below)",
32134
+ "",
32135
+ "CANDIDATES (read-only explorer reports):",
32136
+ ...candidates.map(renderCandidate),
32137
+ "",
32138
+ "Decide which candidate is best supported by the evidence above."
32139
+ ].join("\n");
32140
+ }
32141
+ function needsMoreEvidence(rationale, fallbackReason) {
32142
+ return {
32143
+ status: "needs_more_evidence",
32144
+ winnerIndex: null,
32145
+ rationale,
32146
+ requiredChecks: [],
32147
+ degraded: true,
32148
+ ...fallbackReason ? { fallbackReason } : {},
32149
+ verifier: null,
32150
+ judgedBy: "deterministic"
32151
+ };
32152
+ }
32153
+ function parseSelectionVerdict(raw, candidates) {
32154
+ const open = raw.lastIndexOf(VERDICT_OPEN);
32155
+ const close = raw.lastIndexOf(VERDICT_CLOSE);
32156
+ if (open === -1 || close === -1 || close < open) {
32157
+ return { ok: false, error: "missing verdict block" };
32158
+ }
32159
+ const body = raw.slice(open + VERDICT_OPEN.length, close).trim();
32160
+ let parsed;
32161
+ try {
32162
+ parsed = JSON.parse(body);
32163
+ } catch (err) {
32164
+ return { ok: false, error: `invalid JSON (${err instanceof Error ? err.message : String(err)})` };
32165
+ }
32166
+ if (typeof parsed !== "object" || parsed === null) {
32167
+ return { ok: false, error: "verdict is not a JSON object" };
32168
+ }
32169
+ const obj = parsed;
32170
+ const status = obj.status;
32171
+ if (status !== "selected" && status !== "needs_more_evidence") {
32172
+ return { ok: false, error: `invalid status (${String(status)})` };
32173
+ }
32174
+ const rationale = typeof obj.rationale === "string" ? obj.rationale.trim() : "";
32175
+ const requiredChecks = (Array.isArray(obj.requiredChecks) ? obj.requiredChecks : []).filter((c) => typeof c === "string" && c.trim().length > 0).map((c) => c.trim()).slice(0, 5);
32176
+ if (status === "needs_more_evidence") {
32177
+ return {
32178
+ ok: true,
32179
+ verdict: {
32180
+ status,
32181
+ winnerIndex: null,
32182
+ rationale: rationale || "Candidates are not sufficiently differentiated by evidence.",
32183
+ requiredChecks,
32184
+ degraded: false,
32185
+ verifier: null,
32186
+ judgedBy: "llm"
32187
+ }
32188
+ };
32189
+ }
32190
+ const winner = obj.winnerIndex;
32191
+ if (typeof winner !== "number" || !Number.isInteger(winner)) {
32192
+ return { ok: false, error: `selected without integer winnerIndex (${String(winner)})` };
32193
+ }
32194
+ const entry = candidates.find((c) => c.index === winner);
32195
+ if (!entry || entry.status !== "ok") {
32196
+ return { ok: false, error: `winnerIndex ${winner} does not point at a usable candidate` };
32197
+ }
32198
+ return {
32199
+ ok: true,
32200
+ verdict: {
32201
+ status: "selected",
32202
+ winnerIndex: winner,
32203
+ rationale: rationale || "Selected by evidence comparison.",
32204
+ requiredChecks,
32205
+ degraded: false,
32206
+ verifier: null,
32207
+ judgedBy: "llm"
32208
+ }
32209
+ };
32210
+ }
32211
+ async function runKrakenSelection(opts) {
32212
+ const okEntries = opts.candidates.filter(
32213
+ (c) => c.status === "ok"
32214
+ );
32215
+ if (okEntries.length === 0) {
32216
+ return needsMoreEvidence(
32217
+ "No usable candidate reports this turn (all malformed). Proceed with your own judgment.",
32218
+ "no usable candidates"
32219
+ );
32220
+ }
32221
+ if (okEntries.length === 1) {
32222
+ return {
32223
+ status: "selected",
32224
+ winnerIndex: okEntries[0].index,
32225
+ rationale: "Single usable candidate this turn \u2014 selected without a comparison call.",
32226
+ requiredChecks: [],
32227
+ degraded: false,
32228
+ verifier: null,
32229
+ judgedBy: "deterministic"
32230
+ };
32231
+ }
32232
+ const system = VERIFIER_SYSTEM_PROMPT;
32233
+ const user = buildSelectionPrompt(opts.task, opts.candidates);
32234
+ let raw;
32235
+ try {
32236
+ raw = await opts.callModel({ system, user, identity: opts.identity });
32237
+ } catch (err) {
32238
+ return needsMoreEvidence(
32239
+ `Verifier call failed (${err instanceof Error ? err.message : String(err)}). Proceed with your own judgment.`,
32240
+ "verifier call failed"
32241
+ );
32242
+ }
32243
+ const parsed = parseSelectionVerdict(raw, opts.candidates);
32244
+ if (!parsed.ok) {
32245
+ return needsMoreEvidence(
32246
+ `Verifier response unusable (${parsed.error}). Proceed with your own judgment.`,
32247
+ `malformed verifier response: ${parsed.error}`
32248
+ );
32249
+ }
32250
+ return { ...parsed.verdict, verifier: opts.identity };
32251
+ }
32252
+ var VERIFIER_SYSTEM_PROMPT, VERDICT_OPEN, VERDICT_CLOSE;
32253
+ var init_verifier = __esm({
32254
+ "src/cli/kraken/verifier.ts"() {
32255
+ "use strict";
32256
+ VERIFIER_SYSTEM_PROMPT = [
32257
+ "You are the Kraken selection verifier. You compare independent research",
32258
+ "reports (candidates) about ONE task and decide which hypothesis is best",
32259
+ "supported by OBSERVED EVIDENCE. You do not explore anything yourself \u2014",
32260
+ "you judge what the candidates actually observed.",
32261
+ "",
32262
+ "RULES:",
32263
+ "- Evidence decides. A specific observation (file:line, command output,",
32264
+ " test result) outweighs any amount of confident wording.",
32265
+ "- An eloquent candidate with no observations must NOT beat a plainly",
32266
+ " worded candidate whose claims are grounded in evidence.",
32267
+ "- Degraded observations (timeouts, empty searches, unavailable backends)",
32268
+ " are NOT proof of absence \u2014 never treat them as confirmation that",
32269
+ " something does not exist or cannot happen.",
32270
+ "- A candidate CONTRADICTED by a concrete observation loses to one",
32271
+ " consistent with it. Weigh listed risks.",
32272
+ "- If the leading candidates are equally (un)supported, answer",
32273
+ " needs_more_evidence \u2014 do not guess.",
32274
+ "- requiredChecks: 0-5 concrete, runnable checks the implementation must",
32275
+ " pass to prove the winning hypothesis (test names, commands, behaviors).",
32276
+ "",
32277
+ "Answer with EXACTLY this block (valid JSON, no prose before/after):",
32278
+ "<selection-verdict>",
32279
+ "{",
32280
+ ' "status": "selected" | "needs_more_evidence",',
32281
+ ' "winnerIndex": <1-based index of the winning candidate, or null>,',
32282
+ ' "rationale": "2-4 sentences grounded in the evidence",',
32283
+ ' "requiredChecks": ["concrete check", "..."]',
32284
+ "}",
32285
+ "</selection-verdict>"
32286
+ ].join("\n");
32287
+ VERDICT_OPEN = "<selection-verdict>";
32288
+ VERDICT_CLOSE = "</selection-verdict>";
32289
+ }
32290
+ });
32291
+
32292
+ // src/cli/tools/krakenSelectTool.ts
32293
+ async function collectProviderText(stream, params) {
32294
+ let text = "";
32295
+ let usage;
32296
+ for await (const delta of stream(params)) {
32297
+ if (delta.kind === "text") text += delta.delta;
32298
+ else if (delta.kind === "usage") usage = delta.usage;
32299
+ else if (delta.kind === "error") throw new Error(delta.message);
32300
+ else if (delta.kind === "finish") break;
32301
+ }
32302
+ return { text, ...usage ? { usage } : {} };
32303
+ }
32304
+ function renderVerdict(verdict, candidateCount) {
32305
+ const header = verdict.status === "selected" && verdict.winnerIndex !== null ? `kraken_select: SELECTED candidate #${verdict.winnerIndex}` : "kraken_select: NEEDS MORE EVIDENCE \u2014 no candidate clearly wins";
32306
+ const lines = [header, `Rationale: ${verdict.rationale}`];
32307
+ if (verdict.requiredChecks.length > 0) {
32308
+ lines.push("Required checks (must pass before clean completion):");
32309
+ verdict.requiredChecks.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
32310
+ }
32311
+ if (verdict.degraded && verdict.fallbackReason) {
32312
+ lines.push(
32313
+ `NOTE (degraded): ${verdict.fallbackReason}. Proceed with your own judgment \u2014 the verifier could not decide.`
32314
+ );
32315
+ }
32316
+ lines.push(`Candidates compared: ${candidateCount}.`);
32317
+ return lines.join("\n");
32318
+ }
32319
+ function createKrakenSelectTool(deps) {
32320
+ const timeoutMs = deps.timeoutMs ?? 12e4;
32321
+ return {
32322
+ name: "kraken_select",
32323
+ description: "Compare the candidate research reports spawned this turn (task purpose=candidate) and select the hypothesis best supported by OBSERVED EVIDENCE. A dedicated verifier call (default: the current model) judges grounded vs unsupported claims; degraded observations are never treated as proof of absence. Returns the selected candidate, a rationale, and requiredChecks the implementation must pass. Call it ONCE per turn, after candidate tentacles finished and BEFORE implementing. If it reports needs_more_evidence, either spawn ONE more differentiated candidate or proceed with your own judgment.",
32324
+ permissions: ["read", "network"],
32325
+ timeoutMs: 3e5,
32326
+ inputSchema: KrakenSelectArgsSchema,
32327
+ execute: async (input, _ctx) => {
32328
+ try {
32329
+ const candidates = krakenCandidates();
32330
+ if (candidates.length === 0) {
32331
+ return typedOk({
32332
+ result: "kraken_select: no candidates registered this turn \u2014 nothing to compare. Proceed directly (single-path execution)."
32333
+ });
32334
+ }
32335
+ const parent = await deps.loadParentIdentity();
32336
+ if (!parent) {
32337
+ const verdict2 = {
32338
+ status: "needs_more_evidence",
32339
+ winnerIndex: null,
32340
+ rationale: "Verifier unavailable (no provider identity for this run). Proceed with your own judgment.",
32341
+ requiredChecks: [],
32342
+ degraded: true,
32343
+ fallbackReason: "parent identity unavailable",
32344
+ verifier: null,
32345
+ judgedBy: "deterministic"
32346
+ };
32347
+ setKrakenSelection(verdict2);
32348
+ return typedOk({ result: renderVerdict(verdict2, candidates.length) });
32349
+ }
32350
+ const identity = resolveKrakenVerifier(
32351
+ parent,
32352
+ deps.env ?? process.env,
32353
+ deps.loadVerifierOverride?.()
32354
+ );
32355
+ let judgingStartedAt = Date.now();
32356
+ let judgingUsage;
32357
+ let judgingLatencyMs = 0;
32358
+ const verdict = await runKrakenSelection({
32359
+ task: input.task ?? "",
32360
+ candidates,
32361
+ identity,
32362
+ callModel: async ({ system, user, identity: id }) => {
32363
+ const stream = await deps.loadStream(id.provider);
32364
+ if (!stream) {
32365
+ throw new Error(`no provider config for verifier "${id.provider}"`);
32366
+ }
32367
+ judgingStartedAt = Date.now();
32368
+ try {
32369
+ const { text: raw, usage } = await collectProviderText(stream, {
32370
+ messages: [
32371
+ { role: "system", content: system },
32372
+ { role: "user", content: user }
32373
+ ],
32374
+ model: id.model,
32375
+ provider: id.provider,
32376
+ tools: [],
32377
+ signal: AbortSignal.timeout(timeoutMs)
32378
+ });
32379
+ judgingUsage = usage;
32380
+ if (raw.trim().length === 0) throw new Error("empty verifier response");
32381
+ return raw;
32382
+ } finally {
32383
+ judgingLatencyMs = Date.now() - judgingStartedAt;
32384
+ }
32385
+ }
32386
+ });
32387
+ recordSelectionOutcome({
32388
+ latencyMs: judgingLatencyMs,
32389
+ ...judgingUsage ? { tokens: judgingUsage.totalTokens } : {},
32390
+ degraded: verdict.degraded,
32391
+ fallbackReason: verdict.fallbackReason
32392
+ });
32393
+ setKrakenSelection(verdict);
32394
+ return typedOk({ result: renderVerdict(verdict, candidates.length) });
32395
+ } catch (err) {
32396
+ return typedErr(
32397
+ `kraken_select: internal error \u2014 ${err instanceof Error ? err.message : String(err)}`
32398
+ );
32399
+ }
32400
+ }
32401
+ };
32402
+ }
32403
+ var KrakenSelectArgsSchema;
32404
+ var init_krakenSelectTool = __esm({
32405
+ "src/cli/tools/krakenSelectTool.ts"() {
32406
+ "use strict";
32407
+ init_zod();
32408
+ init_metrics2();
32409
+ init_toolTypes();
32410
+ init_verifier();
32411
+ init_candidateRegistry();
32412
+ KrakenSelectArgsSchema = external_exports.object({
32413
+ task: external_exports.string().optional().describe(
32414
+ "The user task being solved, in one or two sentences. Used to judge which candidate hypothesis best serves it. Omit to judge against the candidates alone."
32415
+ )
32416
+ });
31527
32417
  }
31528
32418
  });
31529
32419
 
@@ -32248,13 +33138,13 @@ async function withPlanStore(projectRoot, fn) {
32248
33138
  return out;
32249
33139
  });
32250
33140
  }
32251
- function nextPlanTaskId(store4) {
32252
- const maxExisting = store4.tasks.reduce((max, t) => {
33141
+ function nextPlanTaskId(store6) {
33142
+ const maxExisting = store6.tasks.reduce((max, t) => {
32253
33143
  const m = /^t(\d+)$/.exec(t.id);
32254
33144
  return m ? Math.max(max, parseInt(m[1], 10)) : max;
32255
33145
  }, 0);
32256
- store4.counter = Math.max(store4.counter, maxExisting) + 1;
32257
- return `t${store4.counter}`;
33146
+ store6.counter = Math.max(store6.counter, maxExisting) + 1;
33147
+ return `t${store6.counter}`;
32258
33148
  }
32259
33149
  function writePlanTaskArtifact(rootDir, task) {
32260
33150
  const path56 = join15(rootDir, "plan-tasks", `${task.id}.md`);
@@ -32434,14 +33324,14 @@ function createPlanTaskTools(opts) {
32434
33324
  inputSchema: CreateSchema,
32435
33325
  execute: async (input) => {
32436
33326
  try {
32437
- const res = await withPlanStore(projectRoot, (store4) => {
32438
- if (store4.tasks.length >= PLAN_MAX_TASKS) {
33327
+ const res = await withPlanStore(projectRoot, (store6) => {
33328
+ if (store6.tasks.length >= PLAN_MAX_TASKS) {
32439
33329
  return typedErr(
32440
- `PLAN_TOO_MANY_TASKS: plan.json already holds ${store4.tasks.length} tasks (max ${PLAN_MAX_TASKS}).`
33330
+ `PLAN_TOO_MANY_TASKS: plan.json already holds ${store6.tasks.length} tasks (max ${PLAN_MAX_TASKS}).`
32441
33331
  );
32442
33332
  }
32443
33333
  const now = (/* @__PURE__ */ new Date()).toISOString();
32444
- const id = nextPlanTaskId(store4);
33334
+ const id = nextPlanTaskId(store6);
32445
33335
  const task = {
32446
33336
  id,
32447
33337
  title: input.title.trim().slice(0, PLAN_TITLE_MAX),
@@ -32455,8 +33345,8 @@ function createPlanTaskTools(opts) {
32455
33345
  createdAt: now,
32456
33346
  updatedAt: now
32457
33347
  };
32458
- store4.tasks.push(task);
32459
- writePlanTaskArtifact(store4.rootDir, task);
33348
+ store6.tasks.push(task);
33349
+ writePlanTaskArtifact(store6.rootDir, task);
32460
33350
  return typedOk({ id, task });
32461
33351
  });
32462
33352
  if (res.ok) {
@@ -32480,8 +33370,8 @@ function createPlanTaskTools(opts) {
32480
33370
  inputSchema: UpdateSchema,
32481
33371
  execute: async (input) => {
32482
33372
  try {
32483
- const res = await withPlanStore(projectRoot, (store4) => {
32484
- const task = store4.tasks.find((t) => t.id === input.id);
33373
+ const res = await withPlanStore(projectRoot, (store6) => {
33374
+ const task = store6.tasks.find((t) => t.id === input.id);
32485
33375
  if (!task) {
32486
33376
  return typedErr(
32487
33377
  `PLAN_TASK_NOT_FOUND: no task with id "${input.id}" in .zelari/plan.json (call task_list for current ids).`
@@ -32504,7 +33394,7 @@ ${input.appendNote}` : input.appendNote;
32504
33394
  task.notes = merged.slice(-PLAN_NOTES_MAX);
32505
33395
  }
32506
33396
  task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
32507
- writePlanTaskArtifact(store4.rootDir, task);
33397
+ writePlanTaskArtifact(store6.rootDir, task);
32508
33398
  return typedOk({ task });
32509
33399
  });
32510
33400
  if (res.ok) {
@@ -32529,21 +33419,21 @@ ${input.appendNote}` : input.appendNote;
32529
33419
  execute: async (input) => {
32530
33420
  try {
32531
33421
  let allPayloads = [];
32532
- const res = await withPlanStore(projectRoot, (store4) => {
32533
- allPayloads = store4.tasks.map(toTaskPayload);
32534
- const filtered = store4.tasks.filter(
33422
+ const res = await withPlanStore(projectRoot, (store6) => {
33423
+ allPayloads = store6.tasks.map(toTaskPayload);
33424
+ const filtered = store6.tasks.filter(
32535
33425
  (t) => (input.status === void 0 || t.status === input.status) && (input.phaseId === void 0 || t.phaseId === input.phaseId)
32536
33426
  );
32537
- const done = store4.tasks.filter(
33427
+ const done = store6.tasks.filter(
32538
33428
  (t) => t.status === "completed" || t.status === "cancelled"
32539
33429
  ).length;
32540
33430
  const formatted = filtered.length === 0 ? "(no matching workspace tasks)" : filtered.map(taskSummaryLine).join("\n");
32541
33431
  return typedOk({
32542
33432
  tasks: filtered,
32543
- total: store4.tasks.length,
33433
+ total: store6.tasks.length,
32544
33434
  done,
32545
33435
  formatted: `${formatted}
32546
- (done/total: ${done}/${store4.tasks.length})`
33436
+ (done/total: ${done}/${store6.tasks.length})`
32547
33437
  });
32548
33438
  });
32549
33439
  if (res.ok) {
@@ -35673,7 +36563,7 @@ import path31 from "node:path";
35673
36563
  function trustStorePath() {
35674
36564
  return _overrideStorePath ?? path31.join(homedir8(), ".zelari-code", "trust.json");
35675
36565
  }
35676
- function normalize(p3) {
36566
+ function normalize3(p3) {
35677
36567
  const resolved = path31.resolve(p3);
35678
36568
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
35679
36569
  }
@@ -35687,11 +36577,11 @@ function readStore3() {
35687
36577
  return DEFAULT_STORE;
35688
36578
  }
35689
36579
  }
35690
- function writeStore3(store4) {
36580
+ function writeStore3(store6) {
35691
36581
  const p3 = trustStorePath();
35692
36582
  try {
35693
36583
  mkdirSync14(path31.dirname(p3), { recursive: true });
35694
- writeFileSync16(p3, JSON.stringify(store4, null, 2), "utf8");
36584
+ writeFileSync16(p3, JSON.stringify(store6, null, 2), "utf8");
35695
36585
  } catch (err) {
35696
36586
  throw new Error(
35697
36587
  `failed to persist trust store ${p3}: ${err instanceof Error ? err.message : String(err)}`
@@ -35710,26 +36600,26 @@ function isFolderTrusted(folderPath) {
35710
36600
  const env = envTrustedFolder();
35711
36601
  if (env === "all") return true;
35712
36602
  if (env === "none") return false;
35713
- if (env) return normalize(env) === normalize(folderPath);
35714
- const target = normalize(folderPath);
35715
- return readStore3().folders.some((f) => normalize(f.path) === target);
36603
+ if (env) return normalize3(env) === normalize3(folderPath);
36604
+ const target = normalize3(folderPath);
36605
+ return readStore3().folders.some((f) => normalize3(f.path) === target);
35716
36606
  }
35717
36607
  function trustFolder(folderPath) {
35718
- const store4 = readStore3();
36608
+ const store6 = readStore3();
35719
36609
  const normalized = path31.resolve(folderPath);
35720
- if (!store4.folders.some((f) => normalize(f.path) === normalize(normalized))) {
35721
- store4.folders.push({ path: normalized, trustedAt: (/* @__PURE__ */ new Date()).toISOString() });
35722
- writeStore3(store4);
36610
+ if (!store6.folders.some((f) => normalize3(f.path) === normalize3(normalized))) {
36611
+ store6.folders.push({ path: normalized, trustedAt: (/* @__PURE__ */ new Date()).toISOString() });
36612
+ writeStore3(store6);
35723
36613
  }
35724
36614
  return { ok: true, path: normalized };
35725
36615
  }
35726
36616
  function untrustFolder(folderPath) {
35727
- const store4 = readStore3();
35728
- const target = normalize(folderPath);
35729
- const before = store4.folders.length;
35730
- store4.folders = store4.folders.filter((f) => normalize(f.path) !== target);
35731
- if (store4.folders.length === before) return { ok: true, removed: false };
35732
- writeStore3(store4);
36617
+ const store6 = readStore3();
36618
+ const target = normalize3(folderPath);
36619
+ const before = store6.folders.length;
36620
+ store6.folders = store6.folders.filter((f) => normalize3(f.path) !== target);
36621
+ if (store6.folders.length === before) return { ok: true, removed: false };
36622
+ writeStore3(store6);
35733
36623
  return { ok: true, removed: true };
35734
36624
  }
35735
36625
  function listTrustedFolders() {
@@ -35884,19 +36774,19 @@ function applyTruncation(result, toolName) {
35884
36774
  function evictOldest() {
35885
36775
  let oldestKey = null;
35886
36776
  let oldestTs = Infinity;
35887
- for (const [key, entry] of store2) {
36777
+ for (const [key, entry] of store4) {
35888
36778
  if (entry.ts < oldestTs) {
35889
36779
  oldestTs = entry.ts;
35890
36780
  oldestKey = key;
35891
36781
  }
35892
36782
  }
35893
- if (oldestKey) store2.delete(oldestKey);
36783
+ if (oldestKey) store4.delete(oldestKey);
35894
36784
  }
35895
36785
  function cacheGet(key, now) {
35896
- const entry = store2.get(key);
36786
+ const entry = store4.get(key);
35897
36787
  if (!entry) return null;
35898
36788
  if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
35899
- store2.delete(key);
36789
+ store4.delete(key);
35900
36790
  return null;
35901
36791
  }
35902
36792
  return cloneResult(entry.result) ?? entry.result;
@@ -35906,8 +36796,8 @@ function cachePut(key, result, now, ttlMs) {
35906
36796
  if (resultBytes(result) > TOOL_CACHE_MAX_BYTES) return;
35907
36797
  const cloned = cloneResult(result);
35908
36798
  if (!cloned) return;
35909
- if (store2.size >= TOOL_CACHE_MAX_ENTRIES && !store2.has(key)) evictOldest();
35910
- store2.set(key, {
36799
+ if (store4.size >= TOOL_CACHE_MAX_ENTRIES && !store4.has(key)) evictOldest();
36800
+ store4.set(key, {
35911
36801
  result: cloned,
35912
36802
  ts: now,
35913
36803
  ...ttlMs !== void 0 ? { expiresAt: now + ttlMs } : {}
@@ -35951,7 +36841,7 @@ function withResultCache(tool, options = {}) {
35951
36841
  }
35952
36842
  };
35953
36843
  }
35954
- var TOOL_CACHE_MAX_ENTRIES, TOOL_CACHE_MAX_BYTES, TOOL_CACHE_DEFAULT_TTL_MS, store2;
36844
+ var TOOL_CACHE_MAX_ENTRIES, TOOL_CACHE_MAX_BYTES, TOOL_CACHE_DEFAULT_TTL_MS, store4;
35955
36845
  var init_toolResultCache = __esm({
35956
36846
  "src/cli/toolResultCache.ts"() {
35957
36847
  "use strict";
@@ -35959,7 +36849,7 @@ var init_toolResultCache = __esm({
35959
36849
  TOOL_CACHE_MAX_ENTRIES = 200;
35960
36850
  TOOL_CACHE_MAX_BYTES = 256 * 1024;
35961
36851
  TOOL_CACHE_DEFAULT_TTL_MS = 5 * 60 * 1e3;
35962
- store2 = /* @__PURE__ */ new Map();
36852
+ store4 = /* @__PURE__ */ new Map();
35963
36853
  }
35964
36854
  });
35965
36855
 
@@ -36230,11 +37120,20 @@ function createBuiltinToolRegistry(options = {}) {
36230
37120
  });
36231
37121
  }
36232
37122
  }
36233
- const enableTask = options.enableTask !== false && !readOnly && !verifyMode && profile === "full" && !options.planMode;
37123
+ const enableTask = options.enableTask !== false && options.readOnly !== true && !verifyMode && profile === "full" && (!options.planMode || options.planExploreTask !== false);
36234
37124
  if (enableTask) {
36235
- const taskTool = createTaskTool({
36236
- createSubAgentContext: createKrakenSubAgentContextFactory({ root, audit, sessionId: sessionId2 })
36237
- });
37125
+ const taskTool = createTaskTool(
37126
+ {
37127
+ createSubAgentContext: createKrakenSubAgentContextFactory({
37128
+ root,
37129
+ audit,
37130
+ sessionId: sessionId2,
37131
+ ...options.subAgentProvider ? { provider: options.subAgentProvider } : {},
37132
+ ...options.subAgentModel ? { model: options.subAgentModel } : {}
37133
+ })
37134
+ },
37135
+ options.planMode === true ? { allowedAgents: ["explore"] } : void 0
37136
+ );
36238
37137
  registry4.register(withPerm(taskTool));
36239
37138
  tools.push({
36240
37139
  name: taskTool.name,
@@ -36242,6 +37141,31 @@ function createBuiltinToolRegistry(options = {}) {
36242
37141
  permissions: taskTool.permissions ?? []
36243
37142
  });
36244
37143
  }
37144
+ if (options.krakenSelect === true && enableTask) {
37145
+ const selectTool = createKrakenSelectTool({
37146
+ loadParentIdentity: async () => {
37147
+ const cfg = options.subAgentProvider ? await providerConfigFor(options.subAgentProvider) : await providerFromEnv();
37148
+ if (!cfg) return null;
37149
+ return {
37150
+ provider: cfg.providerId,
37151
+ model: options.subAgentModel || cfg.model
37152
+ };
37153
+ },
37154
+ loadStream: async (provider) => {
37155
+ const cfg = await providerConfigFor(provider);
37156
+ return cfg ? buildProviderStream(cfg) : null;
37157
+ },
37158
+ // Fase 9 (ADR-0020): persisted verifier override (provider.json
37159
+ // `krakenVerifier`). undefined = inherit the EXACT parent model.
37160
+ loadVerifierOverride: () => getKrakenVerifierOverride()
37161
+ });
37162
+ registry4.register(withPerm(selectTool));
37163
+ tools.push({
37164
+ name: selectTool.name,
37165
+ description: selectTool.description,
37166
+ permissions: selectTool.permissions ?? []
37167
+ });
37168
+ }
36245
37169
  if (process.env.ZELARI_LSP !== "0" && options.lspProvider !== null) {
36246
37170
  const lspTools = options.lspProvider ? createLspTools(options.lspProvider, root) : createLspTools(getSharedLspManager(root), root);
36247
37171
  for (const t of lspTools) {
@@ -36524,6 +37448,7 @@ var init_toolRegistry = __esm({
36524
37448
  init_auditLogger();
36525
37449
  init_engine();
36526
37450
  init_taskTool();
37451
+ init_krakenSelectTool();
36527
37452
  init_askUser();
36528
37453
  init_skillTool();
36529
37454
  init_todoTools();
@@ -36540,6 +37465,7 @@ var init_toolRegistry = __esm({
36540
37465
  init_worldModel();
36541
37466
  init_openai_compatible();
36542
37467
  init_resolveStream();
37468
+ init_providerConfig();
36543
37469
  init_toolPermissions();
36544
37470
  init_lifecycleHooks();
36545
37471
  init_toolResultCache();
@@ -36604,10 +37530,10 @@ function isStateEnabled(env = process.env) {
36604
37530
  }
36605
37531
  async function getStateStore(projectRoot, env = process.env) {
36606
37532
  if (!isStateEnabled(env)) return new NoopDurableStateStore();
36607
- const store4 = new FileDurableStateStore();
37533
+ const store6 = new FileDurableStateStore();
36608
37534
  try {
36609
- await store4.init(projectRoot);
36610
- return store4;
37535
+ await store6.init(projectRoot);
37536
+ return store6;
36611
37537
  } catch {
36612
37538
  return new NoopDurableStateStore();
36613
37539
  }
@@ -37256,24 +38182,24 @@ var init_historyCompaction = __esm({
37256
38182
 
37257
38183
  // src/cli/budget/requestSnapshotStore.ts
37258
38184
  function recordRequestSnapshot(sessionId2, snapshot) {
37259
- store3.set(sessionId2, { snapshot });
38185
+ store5.set(sessionId2, { snapshot });
37260
38186
  }
37261
38187
  function recordRequestUsage(sessionId2, usage) {
37262
- const entry = store3.get(sessionId2);
38188
+ const entry = store5.get(sessionId2);
37263
38189
  if (!entry) return;
37264
38190
  entry.usage = usage;
37265
38191
  }
37266
38192
  function getRequestSnapshotWithUsage(sessionId2) {
37267
- return store3.get(sessionId2) ?? null;
38193
+ return store5.get(sessionId2) ?? null;
37268
38194
  }
37269
38195
  function clearAllRequestSnapshots() {
37270
- store3.clear();
38196
+ store5.clear();
37271
38197
  }
37272
- var store3;
38198
+ var store5;
37273
38199
  var init_requestSnapshotStore = __esm({
37274
38200
  "src/cli/budget/requestSnapshotStore.ts"() {
37275
38201
  "use strict";
37276
- store3 = /* @__PURE__ */ new Map();
38202
+ store5 = /* @__PURE__ */ new Map();
37277
38203
  }
37278
38204
  });
37279
38205
 
@@ -38420,8 +39346,8 @@ async function loadDurableContext(projectRoot, opts) {
38420
39346
  return cache2.text;
38421
39347
  }
38422
39348
  try {
38423
- const store4 = await getStateStore(projectRoot, env);
38424
- const text = await store4.materializeContext(void 0, opts?.maxChars);
39349
+ const store6 = await getStateStore(projectRoot, env);
39350
+ const text = await store6.materializeContext(void 0, opts?.maxChars);
38425
39351
  cache2 = { text: text || "", at: now, projectRoot };
38426
39352
  return cache2.text;
38427
39353
  } catch {
@@ -41229,7 +42155,7 @@ __export(commitHelpers_exports, {
41229
42155
  });
41230
42156
  async function tryStateCommit(args) {
41231
42157
  try {
41232
- const store4 = args.store ?? await getStateStore(args.projectRoot, args.env);
42158
+ const store6 = args.store ?? await getStateStore(args.projectRoot, args.env);
41233
42159
  let workspaceCheckpointId = args.workspaceCheckpointId;
41234
42160
  if (!workspaceCheckpointId && args.withCheckpoint && (args.env ?? process.env).ZELARI_CHECKPOINT !== "0") {
41235
42161
  const cp = await createCheckpoint(
@@ -41238,7 +42164,7 @@ async function tryStateCommit(args) {
41238
42164
  );
41239
42165
  if (cp.ok) workspaceCheckpointId = cp.value.id;
41240
42166
  }
41241
- const meta3 = await store4.commit({
42167
+ const meta3 = await store6.commit({
41242
42168
  mode: args.mode,
41243
42169
  label: args.label,
41244
42170
  layer: args.layer,
@@ -45988,6 +46914,9 @@ function parseSetConfigFlags(argv) {
45988
46914
  let endpoint;
45989
46915
  let thinking;
45990
46916
  let endpointClear = false;
46917
+ let verifierProvider;
46918
+ let verifierModel;
46919
+ let verifierClear = false;
45991
46920
  for (let i = 0; i < argv.length; i++) {
45992
46921
  const arg = argv[i];
45993
46922
  if (arg === "--provider") {
@@ -46004,12 +46933,20 @@ function parseSetConfigFlags(argv) {
46004
46933
  i++;
46005
46934
  } else if (arg === "--endpoint-clear") {
46006
46935
  endpointClear = true;
46936
+ } else if (arg === "--verifier-provider") {
46937
+ verifierProvider = argv[i + 1];
46938
+ i++;
46939
+ } else if (arg === "--verifier-model") {
46940
+ verifierModel = argv[i + 1];
46941
+ i++;
46942
+ } else if (arg === "--verifier-clear") {
46943
+ verifierClear = true;
46007
46944
  }
46008
46945
  }
46009
- if (!provider && !model && !endpoint && !endpointClear && !thinking) {
46946
+ if (!provider && !model && !endpoint && !endpointClear && !thinking && !verifierProvider && !verifierModel && !verifierClear) {
46010
46947
  return {
46011
46948
  request: null,
46012
- error: "--set-config requires --provider, --model, --endpoint, --thinking, and/or --endpoint-clear"
46949
+ error: "--set-config requires --provider, --model, --endpoint, --thinking, --verifier-provider + --verifier-model, --verifier-clear, and/or --endpoint-clear"
46013
46950
  };
46014
46951
  }
46015
46952
  if (provider !== void 0 && provider.trim().length === 0) {
@@ -46021,6 +46958,18 @@ function parseSetConfigFlags(argv) {
46021
46958
  if (endpoint !== void 0 && endpoint.trim().length === 0) {
46022
46959
  return { request: null, error: "--endpoint cannot be empty" };
46023
46960
  }
46961
+ if (verifierClear && (verifierProvider || verifierModel)) {
46962
+ return { request: null, error: "--verifier-clear conflicts with --verifier-provider/--verifier-model" };
46963
+ }
46964
+ if (verifierProvider !== void 0 && verifierModel === void 0 || verifierProvider === void 0 && verifierModel !== void 0) {
46965
+ return { request: null, error: "--verifier-provider and --verifier-model must be used together" };
46966
+ }
46967
+ if (verifierProvider !== void 0 && verifierProvider.trim().length === 0) {
46968
+ return { request: null, error: "--verifier-provider cannot be empty" };
46969
+ }
46970
+ if (verifierModel !== void 0 && verifierModel.trim().length === 0) {
46971
+ return { request: null, error: "--verifier-model cannot be empty" };
46972
+ }
46024
46973
  if (endpoint && endpointClear) {
46025
46974
  return { request: null, error: "--endpoint and --endpoint-clear conflict" };
46026
46975
  }
@@ -46033,7 +46982,10 @@ function parseSetConfigFlags(argv) {
46033
46982
  model: model?.trim(),
46034
46983
  endpoint: endpoint?.trim(),
46035
46984
  endpointClear: endpointClear || void 0,
46036
- thinking: thinking?.trim().toLowerCase()
46985
+ thinking: thinking?.trim().toLowerCase(),
46986
+ verifierProvider: verifierProvider?.trim(),
46987
+ verifierModel: verifierModel?.trim(),
46988
+ verifierClear: verifierClear || void 0
46037
46989
  }
46038
46990
  };
46039
46991
  }
@@ -46122,6 +47074,7 @@ function buildDesktopConfigSnapshot() {
46122
47074
  activeProviderId: config2.activeProviderId,
46123
47075
  modelByProvider: { ...config2.modelByProvider },
46124
47076
  providers,
47077
+ krakenVerifier: config2.krakenVerifier ?? null,
46125
47078
  cliVersion: getCurrentVersion(),
46126
47079
  configPaths: {
46127
47080
  provider: getProviderConfigPath(),
@@ -46160,11 +47113,25 @@ function applySetConfig(req) {
46160
47113
  if (req.thinking) {
46161
47114
  setThinkingForProvider(targetProvider, parseThinkingSpec(req.thinking));
46162
47115
  }
47116
+ if (req.verifierClear) {
47117
+ clearKrakenVerifier();
47118
+ }
47119
+ if (req.verifierProvider && req.verifierModel) {
47120
+ const existsV = PROVIDERS.some((p3) => p3.id === req.verifierProvider);
47121
+ if (!existsV) {
47122
+ return {
47123
+ ok: false,
47124
+ error: `unknown verifier provider '${req.verifierProvider}'. Available: ${PROVIDERS.map((p3) => p3.id).join(", ")}`
47125
+ };
47126
+ }
47127
+ setKrakenVerifier(req.verifierProvider, req.verifierModel);
47128
+ }
46163
47129
  const after = getProviderConfig();
47130
+ const vf = after.krakenVerifier;
46164
47131
  const ep = getCustomEndpoint(after.activeProviderId);
46165
47132
  return {
46166
47133
  ok: true,
46167
- message: `activeProvider=${after.activeProviderId} model=${after.modelByProvider[after.activeProviderId]}` + (ep ? ` endpoint=${ep}` : "")
47134
+ message: `activeProvider=${after.activeProviderId} model=${after.modelByProvider[after.activeProviderId]}` + (ep ? ` endpoint=${ep}` : "") + (vf ? ` verifier=${vf.provider}/${vf.model}` : " verifier=inherit")
46168
47135
  };
46169
47136
  } catch (err) {
46170
47137
  return {
@@ -51268,7 +52235,180 @@ init_shellResolver();
51268
52235
  init_keyStore();
51269
52236
  init_providerConfig();
51270
52237
  init_toolRegistry();
52238
+
52239
+ // src/cli/kraken/turnRuntime.ts
52240
+ init_events2();
52241
+ var WRITE_TOOLS3 = /* @__PURE__ */ new Set([
52242
+ "write_file",
52243
+ "edit_file",
52244
+ "apply_diff"
52245
+ ]);
52246
+ var KrakenTurnRuntime = class {
52247
+ mode;
52248
+ sessionId;
52249
+ onProgress;
52250
+ now;
52251
+ loadCheckTotal;
52252
+ loadChecksPassed;
52253
+ phase = "understanding";
52254
+ phaseEnteredAt;
52255
+ tentacles = 0;
52256
+ exploreTentacles = 0;
52257
+ verifyTentacles = 0;
52258
+ writes = 0;
52259
+ inFlightTentacles = 0;
52260
+ /** Required checks registered via kraken_select (Fase 6). */
52261
+ checkTotal;
52262
+ /** Required checks explicitly PASSED (Fase 7; undefined until first report). */
52263
+ checksPassed;
52264
+ /** toolCallId → toolName (end events omit the name). */
52265
+ pendingTools = /* @__PURE__ */ new Map();
52266
+ /** toolCallIds of VERIFY tentacles (Fase 7 refresh trigger). */
52267
+ pendingVerifyIds = /* @__PURE__ */ new Set();
52268
+ started = false;
52269
+ constructor(opts) {
52270
+ this.mode = opts.mode;
52271
+ this.sessionId = opts.sessionId;
52272
+ this.onProgress = opts.onProgress;
52273
+ this.now = opts.now ?? Date.now;
52274
+ this.loadCheckTotal = opts.loadCheckTotal;
52275
+ this.loadChecksPassed = opts.loadChecksPassed;
52276
+ this.phaseEnteredAt = this.now();
52277
+ }
52278
+ /** Start the turn: emits the initial `understanding` event. */
52279
+ beginTurn() {
52280
+ this.started = true;
52281
+ this.emit();
52282
+ }
52283
+ /**
52284
+ * Reset the phase machine for a recovery pass of the SAME turn (headless
52285
+ * BUILD write-retry). Counters are kept — they describe the whole turn.
52286
+ *
52287
+ * Fase 8 (ADR-0020): `beginPass(true)` marks an automatic REPAIR pass
52288
+ * (completion gate) and projects `repairing` instead of restarting at
52289
+ * `understanding`, so the UI shows the fix loop rather than a fresh turn.
52290
+ */
52291
+ beginPass(repair = false) {
52292
+ this.transition(repair ? "repairing" : "understanding");
52293
+ }
52294
+ /**
52295
+ * Terminal transition. `completed` is only projected on a clean finish —
52296
+ * cancellations and errors are already carried by the `agent_end` event.
52297
+ */
52298
+ finish(reason) {
52299
+ if (reason === "completed") this.transition("completed");
52300
+ }
52301
+ /** Feed one parent-turn BrainEvent. Never throws. */
52302
+ observe(event) {
52303
+ try {
52304
+ if (event.type === "tool_execution_start") {
52305
+ if (event.toolCallId && event.toolName) {
52306
+ this.pendingTools.set(event.toolCallId, event.toolName);
52307
+ }
52308
+ if (event.toolName === "kraken_select") {
52309
+ this.transition("selecting");
52310
+ return;
52311
+ }
52312
+ if (event.toolName === "task") {
52313
+ this.tentacles++;
52314
+ this.inFlightTentacles++;
52315
+ const agent = typeof event.args?.agent === "string" ? event.args.agent : "explore";
52316
+ if (agent === "verify") {
52317
+ this.verifyTentacles++;
52318
+ if (event.toolCallId) this.pendingVerifyIds.add(event.toolCallId);
52319
+ } else this.exploreTentacles++;
52320
+ this.transition(agent === "verify" ? "verifying" : "exploring");
52321
+ }
52322
+ return;
52323
+ }
52324
+ if (event.type === "tool_execution_end") {
52325
+ const name = this.pendingTools.get(event.toolCallId);
52326
+ this.pendingTools.delete(event.toolCallId);
52327
+ if (name === "task") {
52328
+ const wasVerify = event.toolCallId ? this.pendingVerifyIds.delete(event.toolCallId) : false;
52329
+ this.inFlightTentacles = Math.max(0, this.inFlightTentacles - 1);
52330
+ let checksChanged = false;
52331
+ if (wasVerify && this.loadChecksPassed) {
52332
+ const passed = this.loadChecksPassed();
52333
+ if (passed !== void 0 && passed !== this.checksPassed) {
52334
+ this.checksPassed = passed;
52335
+ checksChanged = true;
52336
+ }
52337
+ }
52338
+ if (this.inFlightTentacles === 0) {
52339
+ if (this.mode === "plan") this.transition("planning");
52340
+ else if (this.writes > 0) this.transition("implementing");
52341
+ else if (checksChanged) this.emit();
52342
+ } else if (checksChanged) {
52343
+ this.emit();
52344
+ }
52345
+ return;
52346
+ }
52347
+ if (name === "kraken_select") {
52348
+ const total = this.loadCheckTotal?.() ?? 0;
52349
+ if (total > 0 && total !== this.checkTotal) {
52350
+ this.checkTotal = total;
52351
+ this.emit();
52352
+ }
52353
+ return;
52354
+ }
52355
+ if (name && WRITE_TOOLS3.has(name) && !event.isError) {
52356
+ this.writes++;
52357
+ if (this.mode === "build") this.transition("implementing");
52358
+ }
52359
+ return;
52360
+ }
52361
+ } catch {
52362
+ }
52363
+ }
52364
+ /** Current progress payload (counters + phase). */
52365
+ snapshot() {
52366
+ return {
52367
+ phase: this.phase,
52368
+ mode: this.mode,
52369
+ tentacles: this.tentacles,
52370
+ exploreTentacles: this.exploreTentacles,
52371
+ verifyTentacles: this.verifyTentacles,
52372
+ writes: this.writes,
52373
+ phaseEnteredAt: this.phaseEnteredAt,
52374
+ ...this.checkTotal !== void 0 ? { checkTotal: this.checkTotal } : {},
52375
+ ...this.checksPassed !== void 0 ? { checksPassed: this.checksPassed } : {}
52376
+ };
52377
+ }
52378
+ transition(next) {
52379
+ if (this.phase === next) return;
52380
+ this.phase = next;
52381
+ this.phaseEnteredAt = this.now();
52382
+ this.emit();
52383
+ }
52384
+ emit() {
52385
+ if (!this.started) return;
52386
+ try {
52387
+ this.onProgress(
52388
+ createBrainEvent("kraken_progress", this.sessionId, {
52389
+ progress: this.snapshot()
52390
+ })
52391
+ );
52392
+ } catch {
52393
+ }
52394
+ }
52395
+ };
52396
+
52397
+ // src/cli/hooks/useChatTurn.ts
51271
52398
  init_taskTool();
52399
+ init_candidateRegistry();
52400
+ init_metrics2();
52401
+
52402
+ // src/cli/kraken/selectionPlaybook.ts
52403
+ init_skills2();
52404
+ init_candidateRegistry();
52405
+ function krakenSelectionPlaybook(include) {
52406
+ if (!include || !isKrakenSelectionEnabled()) return [];
52407
+ return [KRAKEN_SELECTION_PLAYBOOK_MODULE];
52408
+ }
52409
+
52410
+ // src/cli/hooks/useChatTurn.ts
52411
+ init_completionGate();
51272
52412
 
51273
52413
  // src/cli/hooks/permissionPicker.ts
51274
52414
  init_toolPermissions();
@@ -51623,6 +52763,8 @@ function useChatTurn(params) {
51623
52763
  const dispatchPrompt = useCallback2(
51624
52764
  async (userText, opts) => {
51625
52765
  resetTaskSpawnCount();
52766
+ resetKrakenCandidates();
52767
+ resetKrakenTurnMetrics();
51626
52768
  let envConfig;
51627
52769
  let harness;
51628
52770
  let historySeedLen = 0;
@@ -51706,10 +52848,30 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
51706
52848
  }) : void 0;
51707
52849
  const { registry: toolRegistry } = createBuiltinToolRegistry({
51708
52850
  planMode: workPhase === "plan",
52851
+ // Fase 1 (ADR-0020): anchor tentacles to THIS turn's resolved
52852
+ // provider/model so the TUI selection governs sub-agents too.
52853
+ ...envConfig ? {
52854
+ subAgentProvider: envConfig.providerId,
52855
+ subAgentModel: envConfig.model,
52856
+ // Fase 4 (ADR-0020): kraken_select rides the same alpha
52857
+ // flag as candidate spawning (default off = unchanged).
52858
+ krakenSelect: isKrakenSelectionEnabled()
52859
+ } : {},
51709
52860
  onAskUser,
51710
52861
  onPermissionAsk,
51711
52862
  permissionPolicy: defaultPermissionPolicy()
51712
52863
  });
52864
+ const progressRuntime = new KrakenTurnRuntime({
52865
+ mode: workPhase === "plan" ? "plan" : "build",
52866
+ sessionId: sessionId2,
52867
+ loadCheckTotal: () => krakenRequiredChecks().length,
52868
+ loadChecksPassed: () => krakenChecksPassed(),
52869
+ onProgress: (ev) => {
52870
+ if (writerRef.current) void writerRef.current.append(ev);
52871
+ if (sessionId2) ingestLiveEvent(sessionId2, ev);
52872
+ }
52873
+ });
52874
+ progressRuntime.beginTurn();
51713
52875
  const baseProviderStream = localCliProvider ?? buildProviderStream(envConfig);
51714
52876
  let providerStream;
51715
52877
  if (localCliProvider) {
@@ -51900,6 +53062,7 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
51900
53062
  customPromptModules: [
51901
53063
  KRAKEN_IDENTITY_MODULE,
51902
53064
  KRAKEN_LEAD_PLAYBOOK_MODULE,
53065
+ ...krakenSelectionPlaybook(true),
51903
53066
  languageModule
51904
53067
  ],
51905
53068
  agentSkillConfigs: []
@@ -51968,6 +53131,7 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
51968
53131
  harnessRef.current = harness2;
51969
53132
  setQueueCount(harness2.queueLength);
51970
53133
  let assistantContent = "";
53134
+ let krakenRepairEnqueued = false;
51971
53135
  let streamContent = "";
51972
53136
  const streamScrub = createStreamScrubber(16);
51973
53137
  const toolNameById = /* @__PURE__ */ new Map();
@@ -51976,6 +53140,29 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
51976
53140
  let realUsage = null;
51977
53141
  try {
51978
53142
  for await (const event of harness2.run()) {
53143
+ progressRuntime.observe(event);
53144
+ if (event.type === "agent_end") {
53145
+ let krakenSuppressFinish = false;
53146
+ if (event.reason === "completed" && !krakenRepairEnqueued && isKrakenSelectionEnabled() && workPhase === "build") {
53147
+ const krakenGate = evaluateKrakenCompletionGate("build");
53148
+ if (krakenGate.blocked) {
53149
+ krakenRepairEnqueued = true;
53150
+ markRepairTriggered();
53151
+ harness2.enqueue(buildKrakenRepairPrompt(krakenGate));
53152
+ appendSystem(
53153
+ setMessages,
53154
+ `[kraken] required checks unresolved (${krakenGate.passed}/${krakenGate.total} passed) \u2014 automatic repair pass`,
53155
+ Date.now()
53156
+ );
53157
+ progressRuntime.beginPass(true);
53158
+ krakenSuppressFinish = true;
53159
+ }
53160
+ }
53161
+ if (event.reason === "completed" && krakenRepairEnqueued && !evaluateKrakenCompletionGate("build").blocked) {
53162
+ markRepairSucceeded();
53163
+ }
53164
+ if (!krakenSuppressFinish) progressRuntime.finish(event.reason);
53165
+ }
51979
53166
  if (event.type === "message_end") {
51980
53167
  if (event.usage) realUsage = event.usage;
51981
53168
  if (event.usage) {
@@ -52148,6 +53335,14 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
52148
53335
  }
52149
53336
  }
52150
53337
  }
53338
+ const turnMetrics = collectKrakenTurnMetrics();
53339
+ if (turnMetrics) {
53340
+ const metricsEvent = createBrainEvent("kraken_metrics", sessionId2, {
53341
+ metrics: turnMetrics
53342
+ });
53343
+ if (writerRef.current) void writerRef.current.append(metricsEvent);
53344
+ if (sessionId2) ingestLiveEvent(sessionId2, metricsEvent);
53345
+ }
52151
53346
  turnSucceeded = true;
52152
53347
  } finally {
52153
53348
  flushStreaming();
@@ -54145,12 +55340,12 @@ init_fileStateStore();
54145
55340
  async function restoreDurableState(opts) {
54146
55341
  const restoreTree = opts.restoreTree !== false;
54147
55342
  try {
54148
- const store4 = opts.store ?? await getStateStore(opts.projectRoot);
55343
+ const store6 = opts.store ?? await getStateStore(opts.projectRoot);
54149
55344
  let meta3;
54150
55345
  if (opts.commitId) {
54151
- meta3 = await store4.setHead(opts.commitId);
55346
+ meta3 = await store6.setHead(opts.commitId);
54152
55347
  } else {
54153
- meta3 = await store4.head();
55348
+ meta3 = await store6.head();
54154
55349
  if (!meta3) {
54155
55350
  return {
54156
55351
  ok: false,
@@ -54203,8 +55398,8 @@ function ago2(ms) {
54203
55398
  return `${Math.round(s / 3600)}h ago`;
54204
55399
  }
54205
55400
  async function handleStateStatus(ctx) {
54206
- const store4 = await getStateStore(ctx.cwd);
54207
- const head = await store4.head();
55401
+ const store6 = await getStateStore(ctx.cwd);
55402
+ const head = await store6.head();
54208
55403
  if (!head) {
54209
55404
  appendSystem(
54210
55405
  ctx.setMessages,
@@ -54212,9 +55407,9 @@ async function handleStateStatus(ctx) {
54212
55407
  );
54213
55408
  return;
54214
55409
  }
54215
- const discoveries = await store4.loadDiscoveries(head.id);
55410
+ const discoveries = await store6.loadDiscoveries(head.id);
54216
55411
  const reusable = discoveries.filter((d) => d.reusable).length;
54217
- const recent = await store4.list(8);
55412
+ const recent = await store6.list(8);
54218
55413
  const lines = recent.map((c, i) => {
54219
55414
  const ver2 = c.verification.ran ? c.verification.ok ? "ok" : "fail" : "n/a";
54220
55415
  return ` ${i === 0 ? "\u2192" : " "} ${c.id} ${ago2(c.createdAt)} ${c.label} ver=${ver2}` + (c.layer ? ` [${c.layer}]` : "") + (c.stablePromptHash ? ` hash=${c.stablePromptHash.slice(0, 8)}` : "");
@@ -54233,9 +55428,9 @@ async function handleStateStatus(ctx) {
54233
55428
  );
54234
55429
  }
54235
55430
  async function handleStateCommit(ctx, label) {
54236
- const store4 = await getStateStore(ctx.cwd);
55431
+ const store6 = await getStateStore(ctx.cwd);
54237
55432
  try {
54238
- const meta3 = await store4.commit({
55433
+ const meta3 = await store6.commit({
54239
55434
  mode: "agent",
54240
55435
  label: label?.trim() || "manual state commit",
54241
55436
  layer: "manual",
@@ -54262,8 +55457,8 @@ async function handleStateCommit(ctx, label) {
54262
55457
  }
54263
55458
  }
54264
55459
  async function handleStateShow(ctx, id) {
54265
- const store4 = await getStateStore(ctx.cwd);
54266
- const meta3 = id ? await store4.get(id) : await store4.head();
55460
+ const store6 = await getStateStore(ctx.cwd);
55461
+ const meta3 = id ? await store6.get(id) : await store6.head();
54267
55462
  if (!meta3) {
54268
55463
  appendSystem(
54269
55464
  ctx.setMessages,
@@ -54271,7 +55466,7 @@ async function handleStateShow(ctx, id) {
54271
55466
  );
54272
55467
  return;
54273
55468
  }
54274
- const text = await store4.materializeContext(meta3.id, 6e3);
55469
+ const text = await store6.materializeContext(meta3.id, 6e3);
54275
55470
  appendSystem(ctx.setMessages, `[state] show ${meta3.id}
54276
55471
  ${text}`);
54277
55472
  }
@@ -55781,14 +56976,14 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
55781
56976
  }
55782
56977
  function handleCouncilFeedback(ctx, memberId, score, note) {
55783
56978
  try {
55784
- const store4 = new FeedbackStore();
55785
- const entry = store4.record({
56979
+ const store6 = new FeedbackStore();
56980
+ const entry = store6.record({
55786
56981
  memberId,
55787
56982
  score,
55788
56983
  ...note ? { note } : {},
55789
56984
  ...ctx.sessionId ? { sessionId: ctx.sessionId } : {}
55790
56985
  });
55791
- const stats = store4.getStats(memberId);
56986
+ const stats = store6.getStats(memberId);
55792
56987
  appendSystem(
55793
56988
  ctx.setMessages,
55794
56989
  `[council-feedback] ${memberId} rated ${entry.score}/5 \u2014 running avg ${stats.avg.toFixed(2)} over ${stats.count} rating(s).`
@@ -57480,9 +58675,13 @@ function emitEvent(event) {
57480
58675
  // src/cli/runHeadless.ts
57481
58676
  init_harness();
57482
58677
  init_dist();
58678
+ init_events2();
57483
58679
  init_conversationContext();
57484
58680
  init_council();
57485
58681
  init_toolRegistry();
58682
+ init_candidateRegistry();
58683
+ init_metrics2();
58684
+ init_completionGate();
57486
58685
  init_claudeProvider();
57487
58686
  init_skills2();
57488
58687
  init_envNumber();
@@ -57839,8 +59038,18 @@ async function registerHeadlessMcp(toolRegistry, opts) {
57839
59038
  }
57840
59039
  async function runHeadlessSingle(opts, provider, model, providerStream) {
57841
59040
  const sessionId2 = crypto.randomUUID();
59041
+ resetKrakenCandidates();
59042
+ resetKrakenTurnMetrics();
57842
59043
  const { registry: toolRegistry } = createBuiltinToolRegistry({
57843
59044
  planMode: planModeFromOpts(opts),
59045
+ // Fase 1 (ADR-0020): anchor tentacles to the provider/model THIS run
59046
+ // resolved (--provider/--model opts or Desktop's selector), mirroring
59047
+ // what the kraken-graph path already does for its executor.
59048
+ subAgentProvider: provider,
59049
+ subAgentModel: model,
59050
+ // Fase 4 (ADR-0020): kraken_select on the parent registry for kraken
59051
+ // runs with the alpha selection flag on (default off = unchanged).
59052
+ krakenSelect: opts.mode === "kraken" && isKrakenSelectionEnabled(),
57844
59053
  // ADR-0018 3b: upgrade plan-task domain events to first-class NDJSON
57845
59054
  // BrainEvents. Rust envelopes every stdout line with runId/conversationId,
57846
59055
  // so task events ride the same multiplexed channel as the rest.
@@ -57949,6 +59158,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
57949
59158
  customPromptModules: [
57950
59159
  KRAKEN_IDENTITY_MODULE,
57951
59160
  KRAKEN_LEAD_PLAYBOOK_MODULE,
59161
+ ...krakenSelectionPlaybook(opts.mode === "kraken"),
57952
59162
  {
57953
59163
  type: "language-policy",
57954
59164
  title: "Response Language",
@@ -58013,6 +59223,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
58013
59223
  const scrub = createStreamScrubber2();
58014
59224
  try {
58015
59225
  for await (const event of harness.run()) {
59226
+ progressRuntime.observe(event);
58016
59227
  if (event.type === "message_start") {
58017
59228
  scrub.reset();
58018
59229
  }
@@ -58090,6 +59301,16 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
58090
59301
  messages: harness.getMessages()
58091
59302
  };
58092
59303
  }
59304
+ const progressRuntime = new KrakenTurnRuntime({
59305
+ mode: planModeFromOpts(opts) ? "plan" : "build",
59306
+ sessionId: sessionId2,
59307
+ loadCheckTotal: () => krakenRequiredChecks().length,
59308
+ loadChecksPassed: () => krakenChecksPassed(),
59309
+ onProgress: (ev) => {
59310
+ if (opts.output === "json") emitEvent(ev);
59311
+ }
59312
+ });
59313
+ progressRuntime.beginTurn();
58093
59314
  const initialMessages = [
58094
59315
  ...systemMessages,
58095
59316
  ...historySeed,
@@ -58126,6 +59347,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
58126
59347
  ...retryMessages,
58127
59348
  { role: "user", content: retryPrompt }
58128
59349
  ];
59350
+ progressRuntime.beginPass();
58129
59351
  const retry = await runSinglePass(withSystem, `${sessionId2}-write-retry`);
58130
59352
  pass = {
58131
59353
  ...retry,
@@ -58134,6 +59356,42 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
58134
59356
  emittedWrites: pass.emittedWrites + retry.emittedWrites
58135
59357
  };
58136
59358
  }
59359
+ if (pass.finalReason === "completed" && pass.exitCode === 0 && opts.mode === "kraken" && isKrakenSelectionEnabled() && !planModeFromOpts(opts)) {
59360
+ const gate = evaluateKrakenCompletionGate("build");
59361
+ if (gate.blocked) {
59362
+ const repairPrompt = buildKrakenRepairPrompt(gate);
59363
+ if (opts.output === "json") {
59364
+ emitEvent({
59365
+ type: "log",
59366
+ message: `[headless] Kraken BUILD: ${gate.failedChecks.length} failed / ${gate.unknownChecks.length} unknown required checks \u2014 forcing repair pass`
59367
+ });
59368
+ } else {
59369
+ process.stderr.write(
59370
+ "[zelari-code --headless] Kraken BUILD: required checks unresolved \u2014 forcing repair pass\n"
59371
+ );
59372
+ }
59373
+ const withSystem = [
59374
+ ...systemMessages,
59375
+ ...pass.messages.filter((m) => m.role !== "system"),
59376
+ { role: "user", content: repairPrompt }
59377
+ ];
59378
+ progressRuntime.beginPass(true);
59379
+ markRepairTriggered();
59380
+ const repair = await runSinglePass(withSystem, `${sessionId2}-check-repair`);
59381
+ pass = {
59382
+ ...repair,
59383
+ textBuffer: [...pass.textBuffer, ...repair.textBuffer],
59384
+ successfulWrites: pass.successfulWrites + repair.successfulWrites,
59385
+ emittedWrites: pass.emittedWrites + repair.emittedWrites
59386
+ };
59387
+ if (!evaluateKrakenCompletionGate("build").blocked) markRepairSucceeded();
59388
+ }
59389
+ }
59390
+ progressRuntime.finish(pass.finalReason);
59391
+ const turnMetrics = collectKrakenTurnMetrics();
59392
+ if (turnMetrics && opts.output === "json") {
59393
+ emitEvent(createBrainEvent("kraken_metrics", sessionId2, { metrics: turnMetrics }));
59394
+ }
58137
59395
  if (opts.output === "plain" && pass.textBuffer.length > 0) {
58138
59396
  process.stdout.write(pass.textBuffer.join(""));
58139
59397
  }