zelari-code 2.46.1 → 2.48.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 (84) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/automations/browser/cookieDisk.js +56 -0
  3. package/dist/cli/automations/browser/cookieDisk.js.map +1 -0
  4. package/dist/cli/automations/browser/pageAdapter.js +130 -0
  5. package/dist/cli/automations/browser/pageAdapter.js.map +1 -0
  6. package/dist/cli/automations/browser/selectors.js +178 -0
  7. package/dist/cli/automations/browser/selectors.js.map +1 -0
  8. package/dist/cli/automations/browser/session.js +243 -0
  9. package/dist/cli/automations/browser/session.js.map +1 -0
  10. package/dist/cli/budget/budgetRuntime.js +9 -13
  11. package/dist/cli/budget/budgetRuntime.js.map +1 -1
  12. package/dist/cli/budget/essentialBashConfig.js +118 -0
  13. package/dist/cli/budget/essentialBashConfig.js.map +1 -0
  14. package/dist/cli/headless/runOneTurn.js +36 -6
  15. package/dist/cli/headless/runOneTurn.js.map +1 -1
  16. package/dist/cli/hooks/useChatTurn.js +30 -7
  17. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  18. package/dist/cli/keyStore.js +2 -0
  19. package/dist/cli/keyStore.js.map +1 -1
  20. package/dist/cli/kraken/candidateRegistry.js +13 -0
  21. package/dist/cli/kraken/candidateRegistry.js.map +1 -1
  22. package/dist/cli/kraken/executor.js +200 -22
  23. package/dist/cli/kraken/executor.js.map +1 -1
  24. package/dist/cli/kraken/narrativeFloor.js +23 -0
  25. package/dist/cli/kraken/narrativeFloor.js.map +1 -0
  26. package/dist/cli/kraken/planner.js +27 -0
  27. package/dist/cli/kraken/planner.js.map +1 -1
  28. package/dist/cli/kraken/strictWaiver.js +22 -0
  29. package/dist/cli/kraken/strictWaiver.js.map +1 -0
  30. package/dist/cli/kraken/unresolvedGate.js +32 -0
  31. package/dist/cli/kraken/unresolvedGate.js.map +1 -0
  32. package/dist/cli/kraken/verificationBridge.js +67 -8
  33. package/dist/cli/kraken/verificationBridge.js.map +1 -1
  34. package/dist/cli/kraken/verifyHonestVerdict.js +30 -0
  35. package/dist/cli/kraken/verifyHonestVerdict.js.map +1 -0
  36. package/dist/cli/kraken/worktreeMergeRollback.js +277 -0
  37. package/dist/cli/kraken/worktreeMergeRollback.js.map +1 -0
  38. package/dist/cli/kraken/worktreeScheduling.js +11 -0
  39. package/dist/cli/kraken/worktreeScheduling.js.map +1 -1
  40. package/dist/cli/main.bundled.js +3278 -1483
  41. package/dist/cli/main.bundled.js.map +4 -4
  42. package/dist/cli/main.js +1 -1
  43. package/dist/cli/main.js.map +1 -1
  44. package/dist/cli/modelDiscovery.js +5 -1
  45. package/dist/cli/modelDiscovery.js.map +1 -1
  46. package/dist/cli/museOAuth.js +384 -0
  47. package/dist/cli/museOAuth.js.map +1 -0
  48. package/dist/cli/oauthDesktop.js +18 -1
  49. package/dist/cli/oauthDesktop.js.map +1 -1
  50. package/dist/cli/provider/openai-compatible.js +1 -0
  51. package/dist/cli/provider/openai-compatible.js.map +1 -1
  52. package/dist/cli/provider/resolveStream.js +2 -0
  53. package/dist/cli/provider/resolveStream.js.map +1 -1
  54. package/dist/cli/providerConfig.js +2 -0
  55. package/dist/cli/providerConfig.js.map +1 -1
  56. package/dist/cli/refreshRegistry.js +6 -0
  57. package/dist/cli/refreshRegistry.js.map +1 -1
  58. package/dist/cli/runHeadless.js +184 -100
  59. package/dist/cli/runHeadless.js.map +1 -1
  60. package/dist/cli/slashCommands.js +6 -6
  61. package/dist/cli/slashCommands.js.map +1 -1
  62. package/dist/cli/slashHandlers/krakenGraph.js +25 -7
  63. package/dist/cli/slashHandlers/krakenGraph.js.map +1 -1
  64. package/dist/cli/slashHandlers/provider.js +14 -4
  65. package/dist/cli/slashHandlers/provider.js.map +1 -1
  66. package/dist/cli/thinking.js +1 -0
  67. package/dist/cli/thinking.js.map +1 -1
  68. package/dist/cli/thinkingCapability.js +2 -0
  69. package/dist/cli/thinkingCapability.js.map +1 -1
  70. package/dist/cli/toolRegistry.js +22 -3
  71. package/dist/cli/toolRegistry.js.map +1 -1
  72. package/dist/cli/tools/bashWriteWatch.js +195 -0
  73. package/dist/cli/tools/bashWriteWatch.js.map +1 -0
  74. package/dist/cli/tools/krakenModel.js +50 -0
  75. package/dist/cli/tools/krakenModel.js.map +1 -1
  76. package/dist/cli/tools/krakenRadio.js +50 -5
  77. package/dist/cli/tools/krakenRadio.js.map +1 -1
  78. package/dist/cli/tools/krakenWorktree.js +35 -6
  79. package/dist/cli/tools/krakenWorktree.js.map +1 -1
  80. package/dist/cli/tools/taskTool.js +328 -34
  81. package/dist/cli/tools/taskTool.js.map +1 -1
  82. package/dist/cli/tools/verifyDebtSpine.js +117 -0
  83. package/dist/cli/tools/verifyDebtSpine.js.map +1 -0
  84. package/package.json +2 -2
@@ -26,11 +26,12 @@ import { createKrakenWorktree, cleanupKrakenWorktree, formatWorktreeFooter, isKr
26
26
  import { krakenTentacleStart, krakenTentacleEnd } from './krakenLive.js';
27
27
  import { resolveWorktreeMode } from '../kraken/worktreeScheduling.js';
28
28
  import { randomUUID } from 'node:crypto';
29
- import { candidateInstructions, isKrakenSelectionEnabled, krakenRequiredChecks, parseCandidateReport, registerCandidate, reserveCandidateSlot, setKrakenCheckResults, } from '../kraken/candidateRegistry.js';
29
+ import { candidateInstructions, isKrakenSelectionEnabled, krakenRequiredChecks, parseCandidateReport, registerCandidate, reserveCandidateSlot, setKrakenCheckResults, setLastVerifyToolTrace, getLastVerifyToolTrace, } from '../kraken/candidateRegistry.js';
30
30
  import { allUnknownCheckResults, parseVerifyReport } from '../kraken/verifyReport.js';
31
31
  import { recordCandidateTokens } from '../kraken/metrics.js';
32
32
  import { parseVerifyVerdict } from '@zelari/core';
33
33
  import { startTentacleHeartbeat } from './tentacleHeartbeat.js';
34
+ import { emitVerifyDebtCleared, emitVerifyDebtOpen, enqueueVerifyDebtPersist, loadSessionEventsForVerifyDebt, replayOpenVerifyDebts, } from './verifyDebtSpine.js';
34
35
  /**
35
36
  * Wall-clock bound for the `task` tool wrapper (parent AgentHarness invoke).
36
37
  * Must cover a `general` writer on a slow reasoning model (thinking:max,
@@ -95,24 +96,196 @@ const VERIFY_PROMPT = [
95
96
  'Use status=unknown when you could NOT determine the outcome (degraded',
96
97
  'tool, timeout, inconclusive evidence) — never guess pass.',
97
98
  ].join('\n');
98
- /** Reset spawn counter (call at start of each parent user turn). */
99
- export function resetTaskSpawnCount() {
99
+ /** Sentinel taskId used by the legacy single-slot seam (tests). */
100
+ const SEED_TASK_ID = '__seed__';
101
+ /**
102
+ * K3.3 / F16: bucket key for callers with NO session id (the unit-test seam
103
+ * and pre-session turn boundaries). It IS the legacy `globalThis` slot, so an
104
+ * id-less caller keeps exactly the process-wide behaviour it had before.
105
+ */
106
+ const LEGACY_SESSION_KEY = 'default';
107
+ /** K3.3 / F16: did the caller hand us a real session id? */
108
+ function isSessionScoped(sessionId) {
109
+ return typeof sessionId === 'string' && sessionId.trim().length > 0;
110
+ }
111
+ /** K3.3 / F16: bucket key of a session id (the legacy slot when id-less). */
112
+ function sessionKey(sessionId) {
113
+ return isSessionScoped(sessionId) ? sessionId : LEGACY_SESSION_KEY;
114
+ }
115
+ /** K3.3 / F16: spawn counters of every non-legacy session. */
116
+ function spawnCountMap() {
117
+ const g = globalThis;
118
+ if (!g.__zelariTaskSpawnCountBySession)
119
+ g.__zelariTaskSpawnCountBySession = new Map();
120
+ return g.__zelariTaskSpawnCountBySession;
121
+ }
122
+ /**
123
+ * K3.3 / F16: charge one task spawn to `sessionId` and return THAT session's
124
+ * new count. Counting per session (same numeric cap as before) is what stops a
125
+ * concurrent session in the same process from eating another one's budget.
126
+ */
127
+ function bumpTaskSpawnCount(sessionId) {
128
+ const g = globalThis;
129
+ const key = sessionKey(sessionId);
130
+ if (key === LEGACY_SESSION_KEY) {
131
+ g.__zelariTaskSpawnCount = (g.__zelariTaskSpawnCount ?? 0) + 1;
132
+ return g.__zelariTaskSpawnCount;
133
+ }
134
+ const next = (spawnCountMap().get(key) ?? 0) + 1;
135
+ spawnCountMap().set(key, next);
136
+ return next;
137
+ }
138
+ /**
139
+ * K3.3 / F16: verify-debt bucket of ONE session — the legacy `globalThis` slot
140
+ * for an id-less caller, `__zelariGeneralVerifyDebtBySession` otherwise.
141
+ */
142
+ function debtStore(sessionId) {
143
+ const g = globalThis;
144
+ const key = sessionKey(sessionId);
145
+ if (key === LEGACY_SESSION_KEY) {
146
+ if (!g.__zelariGeneralVerifyDebt)
147
+ g.__zelariGeneralVerifyDebt = new Map();
148
+ return g.__zelariGeneralVerifyDebt;
149
+ }
150
+ if (!g.__zelariGeneralVerifyDebtBySession)
151
+ g.__zelariGeneralVerifyDebtBySession = new Map();
152
+ let bucket = g.__zelariGeneralVerifyDebtBySession.get(key);
153
+ if (!bucket) {
154
+ bucket = new Map();
155
+ g.__zelariGeneralVerifyDebtBySession.set(key, bucket);
156
+ }
157
+ return bucket;
158
+ }
159
+ /** K3.3 / F16: every non-legacy session bucket (aggregate gate reads). */
160
+ function sessionDebtStores() {
161
+ const g = globalThis;
162
+ const bySession = g.__zelariGeneralVerifyDebtBySession;
163
+ return bySession ? [...bySession.values()] : [];
164
+ }
165
+ /**
166
+ * K3.3 / F16: the buckets a read must consult — ONE session when `sessionId`
167
+ * is given, EVERY session (legacy bucket first) when it is omitted. The
168
+ * id-less read is the strict-done gate's view (`runOneTurn.ts` and
169
+ * `useChatTurn.ts` call it with no argument) and stays FAIL-CLOSED: debt open
170
+ * in ANY session blocks the turn.
171
+ */
172
+ function debtScopes(sessionId) {
173
+ return isSessionScoped(sessionId) ? [debtStore(sessionId)] : [debtStore(), ...sessionDebtStores()];
174
+ }
175
+ /**
176
+ * Reset the spawn counter (call at start of each parent user turn).
177
+ *
178
+ * K3.3 / F16: `sessionId` resets ONLY that session's budget and never touches
179
+ * a concurrent session; omitting it keeps the legacy process-wide reset used
180
+ * by the unit-test seam and the headless per-turn boundary.
181
+ */
182
+ export function resetTaskSpawnCount(sessionId) {
100
183
  const g = globalThis;
101
- g.__zelariTaskSpawnCount = 0;
184
+ if (!isSessionScoped(sessionId)) {
185
+ g.__zelariTaskSpawnCount = 0;
186
+ g.__zelariTaskSpawnCountBySession?.clear();
187
+ return;
188
+ }
189
+ if (sessionKey(sessionId) === LEGACY_SESSION_KEY) {
190
+ g.__zelariTaskSpawnCount = 0;
191
+ return;
192
+ }
193
+ g.__zelariTaskSpawnCountBySession?.delete(sessionId);
102
194
  }
103
- /** Reset the general⇒verify obligation (call at start of each parent user turn). */
104
- export function resetTaskVerifyObligation() {
195
+ /**
196
+ * Reset the general⇒verify obligation (call at start of each parent user turn).
197
+ *
198
+ * K3.3 / F16: `sessionId` drops ONLY that session's debt and never a
199
+ * concurrent session's; omitting it keeps the legacy process-wide reset used
200
+ * by the unit-test seam and the headless per-turn boundary.
201
+ */
202
+ export function resetTaskVerifyObligation(sessionId) {
105
203
  const g = globalThis;
106
- g.__zelariGeneralVerifyDebt = null;
204
+ if (!isSessionScoped(sessionId)) {
205
+ g.__zelariGeneralVerifyDebt = new Map();
206
+ g.__zelariGeneralVerifyDebtBySession?.clear();
207
+ return;
208
+ }
209
+ if (sessionKey(sessionId) === LEGACY_SESSION_KEY) {
210
+ g.__zelariGeneralVerifyDebt = new Map();
211
+ return;
212
+ }
213
+ g.__zelariGeneralVerifyDebtBySession?.delete(sessionId);
107
214
  }
108
215
  /**
109
216
  * Open verify obligation, or null when every general this turn has been
110
217
  * verified PASS by the runtime auto-spawn (t78). Consulted by the headless
111
218
  * strict-done gate — an open obligation closes the turn blocked (exit 4).
219
+ *
220
+ * K1.1: returns the first open record (any one is enough to block). The map
221
+ * may carry MORE records than this returns; use `listTaskVerifyObligations()`
222
+ * or `hasOpenTaskVerifyDebt()` to see the full state.
223
+ *
224
+ * K3.3 / F16: scoped to ONE session when `sessionId` is given; an id-less read
225
+ * aggregates every session (fail-closed), so no session's debt is invisible.
112
226
  */
113
- export function taskVerifyObligation() {
114
- const g = globalThis;
115
- return g.__zelariGeneralVerifyDebt ?? null;
227
+ export function taskVerifyObligation(sessionId) {
228
+ for (const store of debtScopes(sessionId)) {
229
+ if (store.size === 0)
230
+ continue;
231
+ // Map iteration is insertion-ordered; the newest insert wins (matches the
232
+ // pre-K1.1 "newest wins" semantics for the single open record).
233
+ const last = store.keys().next().value;
234
+ if (last)
235
+ return store.get(last) ?? null;
236
+ }
237
+ return null;
238
+ }
239
+ /**
240
+ * K1.1: number of open verify obligations (strict-gate invariant: > 0 ⇒
241
+ * blocked). Useful for diagnostics and tests; the strict gate itself keeps
242
+ * using the boolean `taskVerifyObligation() != null` check.
243
+ * K3.3 / F16: `sessionId` scopes the listing (id-less = every session).
244
+ */
245
+ export function listTaskVerifyObligations(sessionId) {
246
+ return debtScopes(sessionId).flatMap((store) => [...store.values()]);
247
+ }
248
+ /**
249
+ * K1.1: did any general leave a runtime verify obligation open? Strict-gate
250
+ * friendly boolean — `true` ⇒ the strict-done gate must block the turn.
251
+ * K3.3 / F16: `sessionId` scopes the check (id-less = every session).
252
+ */
253
+ export function hasOpenTaskVerifyDebt(sessionId) {
254
+ return debtScopes(sessionId).some((store) => store.size > 0);
255
+ }
256
+ /**
257
+ * K1.1: register (or replace) the verify obligation for one specific task.
258
+ * Used by `runAutoVerifyAfterGeneral` to record the debt of EACH general it
259
+ * services — multiple tasks can be open at the same time.
260
+ *
261
+ * K3.3 / F16: `sessionId` writes the debt into THAT session's bucket (id-less
262
+ * callers keep the legacy bucket), so a companion-serve session never opens
263
+ * debt in another session's view.
264
+ */
265
+ export function addTaskVerifyObligation(taskId, debt, sessionId) {
266
+ debtStore(sessionId).set(taskId, debt);
267
+ enqueueVerifyDebtPersist(() => emitVerifyDebtOpen(undefined, {
268
+ taskId,
269
+ description: debt.description,
270
+ detail: debt.detail,
271
+ timestamp: Date.now(),
272
+ }));
273
+ }
274
+ /**
275
+ * K1.1: clear the verify obligation for ONE task. Used on a PASS of THAT
276
+ * task's verify tentacle — clearing is per-task, so a sibling general's
277
+ * debt stays open.
278
+ *
279
+ * K3.3 / F16: clear inside ONE bucket — the session that opened the debt
280
+ * (id-less callers keep the legacy bucket); a concurrent session's debt with
281
+ * the same taskId is never touched.
282
+ */
283
+ export function clearTaskVerifyObligation(taskId, sessionId) {
284
+ const store = debtStore(sessionId);
285
+ if (!store.has(taskId))
286
+ return;
287
+ store.delete(taskId);
288
+ enqueueVerifyDebtPersist(() => emitVerifyDebtCleared(undefined, { taskId }));
116
289
  }
117
290
  /**
118
291
  * F3.3 (verify trust chain): memory only on PASS. `true` when no general⇒verify
@@ -124,17 +297,52 @@ export function taskVerifyObligation() {
124
297
  * and both `promoteOpsKnowledgeSafe` sites on this.
125
298
  */
126
299
  export function outcomeMemoryAllowed() {
127
- return taskVerifyObligation() === null;
300
+ return !hasOpenTaskVerifyDebt();
128
301
  }
129
302
  /**
130
303
  * Test seam: seed an open general⇒verify obligation without running a tentacle.
131
304
  * Production code must never call this — the auto-verify chain owns the slot.
305
+ *
306
+ * K1.1: the optional `taskId` argument disambiguates which slot to seed; when
307
+ * omitted, the legacy sentinel key `'__seed__'` is used so the existing
308
+ * strict-exit test (`runOneTurn.strictExit.test.ts`) keeps working unchanged.
309
+ * K3.3 / F16: the optional `sessionId` seeds that session's bucket (omitted ⇒
310
+ * the legacy bucket, which the id-less strict gate still sees).
132
311
  */
133
- export function seedTaskVerifyObligation(debt) {
134
- const g = globalThis;
135
- g.__zelariGeneralVerifyDebt = debt;
312
+ export function seedTaskVerifyObligation(debt, taskId = SEED_TASK_ID, sessionId) {
313
+ const store = debtStore(sessionId);
314
+ if (debt === null) {
315
+ store.delete(taskId);
316
+ return;
317
+ }
318
+ store.set(taskId, debt);
136
319
  }
137
- /** Max concurrent/serial task spawns per parent turn (env override). */
320
+ /**
321
+ * K1.5 / F5: merge un-cleared `verify.debt_open` events into the process
322
+ * cache WITHOUT emitting (replay must not re-append). Existing slots
323
+ * (including the test seed) are overwritten for matching taskIds and
324
+ * otherwise left alone — callers that want a blank cache reset first.
325
+ *
326
+ * K3.3 / F16: hydrate the bucket of the session the log belongs to
327
+ * (`sessionId`), never the process-wide/other-session view.
328
+ */
329
+ export function hydrateTaskVerifyDebtFromEvents(events, sessionId) {
330
+ const open = replayOpenVerifyDebts(events);
331
+ const store = debtStore(sessionId);
332
+ for (const [taskId, debt] of open) {
333
+ store.set(taskId, debt);
334
+ }
335
+ return open.size;
336
+ }
337
+ /**
338
+ * K1.5: hydrate the cache from `<sessionsDir>/<sessionId>/events.jsonl`.
339
+ * K3.3 / F16: the replay lands in the bucket of the session that owns the log.
340
+ */
341
+ export async function hydrateTaskVerifyDebtFromSpine(source) {
342
+ const events = await loadSessionEventsForVerifyDebt(source);
343
+ return hydrateTaskVerifyDebtFromEvents(events, source.sessionId);
344
+ }
345
+ /** Max concurrent/serial task spawns per parent turn, PER SESSION (env override). */
138
346
  export function maxTaskSpawnsPerTurn() {
139
347
  const raw = process.env.ZELARI_KRAKEN_MAX_TASK_SPAWNS;
140
348
  if (raw === undefined || raw === '')
@@ -256,10 +464,14 @@ async function rememberVerifiedGeneralOutcome(opts) {
256
464
  * the task result (or null when nothing ran — e.g. explore/verify agents).
257
465
  */
258
466
  export async function runAutoVerifyAfterGeneral(opts) {
259
- const g = globalThis;
260
467
  // Debt exists from the moment the general finished — even a mid-chain
261
468
  // failure must not leave the work silently "verified".
262
- g.__zelariGeneralVerifyDebt = { description: opts.original.description };
469
+ //
470
+ // K1.1: key the slot by this general's `agentId` (always populated by
471
+ // TentacleSuccess) so a sibling general's PASS cannot clear our debt.
472
+ const debtKey = opts.general.agentId ?? opts.original.description;
473
+ // K3.3 / F16: the debt is opened in THIS session's bucket (opts.sessionId).
474
+ addTaskVerifyObligation(debtKey, { description: opts.original.description }, opts.sessionId);
263
475
  // t94: live phase captions on the general's activity row (agent_status)
264
476
  // mirrored into the radio 'progress' trail — the parent sees the general
265
477
  // flip to "verifying…" and then to the verdict without polling.
@@ -309,8 +521,20 @@ export async function runAutoVerifyAfterGeneral(opts) {
309
521
  sessionId: opts.sessionId,
310
522
  ...(opts.signal ? { signal: opts.signal } : {}),
311
523
  });
524
+ // K1.3 (rev): every inner-verify return MUST publish its tool trace to
525
+ // the per-turn channel `__zelariVerifyToolTrace` BEFORE we read it back
526
+ // at the floor check, because the inner verify is a direct `runTentacle`
527
+ // call that does NOT go through `createTaskTool.execute` (which is the
528
+ // only other publisher of that channel). Without this publish, (1) an
529
+ // honest PASS with tool captures still fails the floor (positive path
530
+ // broken), and (2) a stale trace from an earlier OUTER verify could be
531
+ // seen here (stale-trace leakage). The helper centralizes the rule.
532
+ const publishInnerVerifyTrace = (result) => {
533
+ setLastVerifyToolTrace(result.ok ? result.toolTrace ?? [] : []);
534
+ };
312
535
  emitVerifyPhase('verifying…');
313
536
  let verify = await runVerify(`verify: ${opts.original.description}`);
537
+ publishInnerVerifyTrace(verify);
314
538
  // Parsed like the graph executor: last VERDICT trailer wins; a failed run is
315
539
  // an unknown (degraded observation is never proof).
316
540
  let verdict = verify.ok ? parseVerifyVerdict(verify.result).verdict : 'unknown';
@@ -342,7 +566,10 @@ export async function runAutoVerifyAfterGeneral(opts) {
342
566
  });
343
567
  if (!rework.ok) {
344
568
  const detail = `rework round ${round} failed: ${rework.error}`;
345
- g.__zelariGeneralVerifyDebt = { description: opts.original.description, detail };
569
+ addTaskVerifyObligation(debtKey, {
570
+ description: opts.original.description,
571
+ detail,
572
+ }, opts.sessionId);
346
573
  appendKrakenRadio(opts.parentCwd, opts.sessionId, {
347
574
  kind: 'error',
348
575
  agent: 'general',
@@ -355,12 +582,36 @@ export async function runAutoVerifyAfterGeneral(opts) {
355
582
  `(${rework.error}). Work stays UNVERIFIED; strict done will close this turn blocked.`);
356
583
  }
357
584
  verify = await runVerify(`verify: ${opts.original.description} (rework ${round})`);
585
+ publishInnerVerifyTrace(verify);
358
586
  verdict = verify.ok ? parseVerifyVerdict(verify.result).verdict : 'unknown';
359
587
  findings = verify.ok ? parseVerifyVerdict(verify.result).findings : '';
360
588
  }
361
589
  }
362
590
  if (verdict === 'pass') {
363
- g.__zelariGeneralVerifyDebt = null;
591
+ // K1.3: the verify trailer may say PASS but the verify must also have
592
+ // executed ≥ 1 tool (real bash / read / etc.) before the debt is cleared.
593
+ // A bare-text PASS — "I read the diff and it's fine" with no tool
594
+ // captures — is narrative-only and cannot clear the obligation.
595
+ const trace = getLastVerifyToolTrace();
596
+ const instrumental = Array.isArray(trace) && trace.length > 0;
597
+ if (!instrumental) {
598
+ const detail = 'verify produced VERDICT: PASS but executed no tool — narrative-only PASS does not satisfy the auto-verify floor';
599
+ addTaskVerifyObligation(debtKey, {
600
+ description: opts.original.description,
601
+ detail,
602
+ }, opts.sessionId);
603
+ emitVerifyPhase('verify PASS without tool evidence', false, 'failed');
604
+ appendKrakenRadio(opts.parentCwd, opts.sessionId, {
605
+ kind: 'error',
606
+ agent: 'verify',
607
+ description: `verify: ${opts.original.description}`,
608
+ detail,
609
+ ok: false,
610
+ });
611
+ return (`\n\n[kraken:auto-verify] ${detail}. Work stays UNVERIFIED; ` +
612
+ `strict done will close this turn blocked.`);
613
+ }
614
+ clearTaskVerifyObligation(debtKey, opts.sessionId);
364
615
  await rememberVerifiedGeneralOutcome(opts);
365
616
  emitVerifyPhase('verify PASS', true, 'completed');
366
617
  return `\n\n[kraken:auto-verify] verify PASS — general⇒verify obligation satisfied.`;
@@ -372,7 +623,10 @@ export async function runAutoVerifyAfterGeneral(opts) {
372
623
  : verify.ok
373
624
  ? 'verify produced no parseable VERDICT — unverified'
374
625
  : `verify tentacle failed: ${verify.error}`;
375
- g.__zelariGeneralVerifyDebt = { description: opts.original.description, detail };
626
+ addTaskVerifyObligation(debtKey, {
627
+ description: opts.original.description,
628
+ detail,
629
+ }, opts.sessionId);
376
630
  appendKrakenRadio(opts.parentCwd, opts.sessionId, {
377
631
  kind: 'error',
378
632
  agent: 'verify',
@@ -647,18 +901,42 @@ export async function runTentacle(opts) {
647
901
  // never share the parent tree, and merges stay sequential.
648
902
  let worktree = null;
649
903
  let effectiveCwd = opts.cwdOverride || parentCwd;
904
+ const worktreeMode = resolveWorktreeMode(process.env.ZELARI_KRAKEN_WORKTREE);
650
905
  const wantWt = agent === 'general' &&
651
906
  deps.allowWorktree !== false &&
652
- (isKrakenWorktreeEnabled() ||
653
- resolveWorktreeMode(process.env.ZELARI_KRAKEN_WORKTREE) === 'auto');
907
+ (isKrakenWorktreeEnabled() || worktreeMode === 'auto');
654
908
  if (wantWt) {
655
909
  try {
656
910
  worktree = await createKrakenWorktree(parentCwd, args.description);
657
911
  if (worktree)
658
912
  effectiveCwd = worktree.path;
659
913
  }
660
- catch {
914
+ catch (err) {
915
+ // F12 (K2.4): creation failed → fail OPEN (the tentacle still runs) but
916
+ // make the degradation LOUD. Under ZELARI_KRAKEN_WORKTREE=auto the graph
917
+ // scheduler admits overlapping writers in parallel ONLY because it
918
+ // assumes worktree isolation; falling back silently would run them
919
+ // unisolated in the shared parent tree with no trace. The radio event
920
+ // plus the deps callback let the executor stop rescuing overlapping
921
+ // writers (serial admission) for the rest of the run.
922
+ const reason = err instanceof Error ? err.message : String(err);
661
923
  worktree = null;
924
+ appendKrakenRadio(parentCwd, sessionId, {
925
+ kind: 'worktree.fallback_shared_tree',
926
+ agent,
927
+ thoroughness,
928
+ description: args.description,
929
+ detail: `worktree creation failed — running in the shared parent tree: ${reason}`,
930
+ mode: worktreeMode,
931
+ reason,
932
+ ...(opts.nodeId !== undefined ? { nodeId: opts.nodeId } : {}),
933
+ ok: false,
934
+ });
935
+ deps.onWorktreeFallback?.({
936
+ reason,
937
+ mode: worktreeMode,
938
+ ...(opts.nodeId !== undefined ? { nodeId: opts.nodeId } : {}),
939
+ });
662
940
  }
663
941
  }
664
942
  appendKrakenRadio(parentCwd, sessionId, {
@@ -971,7 +1249,7 @@ export async function runTentacle(opts) {
971
1249
  if (!kept && isKrakenWorktreeAutoMergeEnabled()) {
972
1250
  emitPhase('merging…');
973
1251
  try {
974
- merge = await mergeKrakenWorktree(worktree, { message: `kraken: merge ${args.description.slice(0, 80)}` });
1252
+ merge = await mergeKrakenWorktree(worktree, { message: `kraken: merge ${args.description.slice(0, 80)}`, sessionId });
975
1253
  }
976
1254
  catch (err) {
977
1255
  merge = {
@@ -1034,7 +1312,13 @@ export async function runTentacle(opts) {
1034
1312
  let memoryId;
1035
1313
  if (deps.memoryService && deps.memoryAutoWrite !== false) {
1036
1314
  try {
1037
- const verifyPass = agent === 'verify' && /(?:VERDICT:\s*PASS|status:\s*pass)/i.test(result);
1315
+ // F4 (K1.4): memory only when the CANONICAL trailer parser says PASS.
1316
+ // The previous free regex /(?:VERDICT:\s*PASS|status:\s*pass)/i matched
1317
+ // any "status: pass" string inside the body (tables, quotes, prose) and
1318
+ // could turn an actual trailer `VERDICT: FAIL` into a memory PASS.
1319
+ // `parseVerifyVerdict` is the last-trailer-wins parser used everywhere
1320
+ // else in the engine — reuse it so memory writes share the same gate.
1321
+ const verifyPass = agent === 'verify' && parseVerifyVerdict(result).verdict === 'pass';
1038
1322
  // F3.3 (verify trust chain): memory only on PASS.
1039
1323
  // - verify/verification: written only when its OWN verdict parsed PASS;
1040
1324
  // - general/outcome: NEVER here — a general result is durable only once
@@ -1149,13 +1433,14 @@ export function createTaskTool(deps, policy = {}) {
1149
1433
  candidateSlot = slot.index;
1150
1434
  }
1151
1435
  const thoroughness = args.thoroughness ?? 'medium';
1152
- const sessionId = ctx.sessionId || 'default';
1436
+ const sessionId = ctx.sessionId || LEGACY_SESSION_KEY;
1153
1437
  const parentCwd = ctx.cwd || process.cwd();
1154
- // Per-process spawn cap (Kraken K3). Reset via resetTaskSpawnCount() each parent turn.
1155
- const g = globalThis;
1156
- g.__zelariTaskSpawnCount = (g.__zelariTaskSpawnCount ?? 0) + 1;
1438
+ // Per-SESSION spawn cap (Kraken K3 + K3.3 / F16): the numeric limit is
1439
+ // unchanged, but it is counted per sessionId — a concurrent session in
1440
+ // the same process (companion serve) cannot exhaust this session's
1441
+ // budget. Reset via resetTaskSpawnCount(sessionId) each parent turn.
1157
1442
  const spawnCap = maxTaskSpawnsPerTurn();
1158
- if (g.__zelariTaskSpawnCount > spawnCap) {
1443
+ if (bumpTaskSpawnCount(sessionId) > spawnCap) {
1159
1444
  return typedErr(`task: spawn cap reached (${spawnCap}). Finish the current slice or raise ZELARI_KRAKEN_MAX_TASK_SPAWNS.`);
1160
1445
  }
1161
1446
  const res = await runTentacle({
@@ -1224,11 +1509,18 @@ export function createTaskTool(deps, policy = {}) {
1224
1509
  // never proof. Only runs when a selection exists this turn (required
1225
1510
  // checks come from a `selected` verdict — Fase 6 routing).
1226
1511
  if (agent === 'verify') {
1512
+ // K1.3: ALWAYS anchor the latest verify tentacle's tool trace on the
1513
+ // per-turn channel so the auto-verify floor (≥ 1 tool execution per
1514
+ // PASS) is observable even when no selection ran this turn (no
1515
+ // required-checks path). The strict gate for PASS is
1516
+ // `getLastVerifyToolTrace().length > 0`.
1517
+ const verifyTrace = res.ok ? res.toolTrace ?? [] : [];
1518
+ setLastVerifyToolTrace(verifyTrace);
1227
1519
  const required = krakenRequiredChecks();
1228
1520
  if (required.length > 0) {
1229
1521
  setKrakenCheckResults(res.ok
1230
1522
  ? parseVerifyReport(res.result, required)
1231
- : allUnknownCheckResults(required, `verify tentacle failed: ${res.error}`), res.ok ? res.toolTrace : undefined);
1523
+ : allUnknownCheckResults(required, `verify tentacle failed: ${res.error}`), verifyTrace);
1232
1524
  }
1233
1525
  }
1234
1526
  if (!res.ok)
@@ -1256,11 +1548,13 @@ export function createTaskTool(deps, policy = {}) {
1256
1548
  }
1257
1549
  catch (err) {
1258
1550
  const msg = err instanceof Error ? err.message : String(err);
1259
- const gv = globalThis;
1260
- gv.__zelariGeneralVerifyDebt = {
1551
+ // K1.1: key the debt by the runtime agentId (when present) so it
1552
+ // matches the key opened by runAutoVerifyAfterGeneral; fall back
1553
+ // to the description so an early throw still lands in the same slot.
1554
+ addTaskVerifyObligation(res.agentId ?? args.description, {
1261
1555
  description: args.description,
1262
1556
  detail: `auto-verify chain failed: ${msg}`,
1263
- };
1557
+ }, sessionId);
1264
1558
  result += `\n\n[kraken:auto-verify] auto-verify chain failed (${msg}) — work stays UNVERIFIED.`;
1265
1559
  }
1266
1560
  }