@integrity-labs/agt-cli 0.21.8 → 0.22.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.
@@ -1,31 +1,20 @@
1
1
  import {
2
2
  SUPERVISOR_RESTART_EXIT_CODE,
3
3
  api,
4
- appendDmFooter,
5
- classifyOutput,
6
4
  exchangeApiKey,
7
5
  extractCommandNotFound,
8
- extractFrontmatter,
9
- formatActorId,
10
6
  getApiKey,
11
- getFramework,
12
7
  getHostId,
13
8
  getIntegration,
14
- isParseError,
15
- isResolveError,
16
- isSelfCompletion,
17
- parseDeliveryTarget,
18
9
  provision,
19
10
  provisionIsolationHook,
11
+ provisionOrientHook,
20
12
  provisionStopHook,
21
- requireHost,
22
- resolveChannels,
23
- resolveDmTarget,
24
- wrapScheduledTaskPrompt
25
- } from "../chunk-OLVOTGNB.js";
13
+ requireHost
14
+ } from "../chunk-27FZ5QEI.js";
26
15
  import {
27
16
  findTaskByTemplate,
28
- getProjectDir,
17
+ getProjectDir as getProjectDir2,
29
18
  getReadyTasks,
30
19
  loadSchedulerState,
31
20
  markTaskFired,
@@ -33,16 +22,19 @@ import {
33
22
  } from "../chunk-TS7W4YNI.js";
34
23
  import {
35
24
  buildAllowedTools,
25
+ ensureKanbanLoopArmed,
36
26
  getLastFailureContext,
37
- getProjectDir as getProjectDir2,
27
+ getProjectDir,
38
28
  getSessionState,
39
29
  injectMessage,
40
30
  isAgentIdle,
31
+ isKanbanLoopArmedForCurrentSession,
41
32
  isSessionHealthy,
42
33
  isStaleForToday,
43
34
  parsePsRows,
44
35
  peekCurrentSession,
45
36
  prepareForRespawn,
37
+ readPaneLogTail,
46
38
  reapOrphanChannelMcps,
47
39
  resetRestartCount,
48
40
  resolveClaudeBinary,
@@ -51,7 +43,22 @@ import {
51
43
  stopAllSessionsAndWait,
52
44
  stopPersistentSession,
53
45
  takeZombieDetection
54
- } from "../chunk-4XNYSTH5.js";
46
+ } from "../chunk-DBGGT2MC.js";
47
+ import {
48
+ appendDmFooter,
49
+ classifyOutput,
50
+ extractFrontmatter,
51
+ formatActorId,
52
+ getFramework,
53
+ isParseError,
54
+ isResolveError,
55
+ isSelfCompletion,
56
+ parseDeliveryTarget,
57
+ parseUsageBanner,
58
+ resolveChannels,
59
+ resolveDmTarget,
60
+ wrapScheduledTaskPrompt
61
+ } from "../chunk-BZV2KNHY.js";
55
62
 
56
63
  // src/lib/manager-worker.ts
57
64
  import { createHash as createHash2 } from "crypto";
@@ -423,12 +430,12 @@ function reapMissingMcpSessions(args) {
423
430
  if (!missingRaw.includes(key)) liveKeys.add(key);
424
431
  }
425
432
  for (const key of liveKeys) {
426
- const state2 = presenceReaperState.get(stateKey(codeName, key));
427
- if (state2) {
428
- state2.attempts = 0;
429
- state2.lastSeenLiveAt = now();
430
- state2.lastAttemptedSessionStartedAt = null;
431
- state2.gaveUpLogged = false;
433
+ const state3 = presenceReaperState.get(stateKey(codeName, key));
434
+ if (state3) {
435
+ state3.attempts = 0;
436
+ state3.lastSeenLiveAt = now();
437
+ state3.lastAttemptedSessionStartedAt = null;
438
+ state3.gaveUpLogged = false;
432
439
  }
433
440
  }
434
441
  if (missing.length === 0) {
@@ -443,24 +450,24 @@ function reapMissingMcpSessions(args) {
443
450
  const active = [];
444
451
  for (const key of missing) {
445
452
  const sk = stateKey(codeName, key);
446
- const state2 = presenceReaperState.get(sk) ?? {
453
+ const state3 = presenceReaperState.get(sk) ?? {
447
454
  attempts: 0,
448
455
  lastSeenLiveAt: null,
449
456
  lastAttemptedSessionStartedAt: null,
450
457
  gaveUpLogged: false
451
458
  };
452
- if (state2.lastAttemptedSessionStartedAt !== sessionStartedAt) {
453
- state2.attempts += 1;
454
- state2.lastAttemptedSessionStartedAt = sessionStartedAt;
459
+ if (state3.lastAttemptedSessionStartedAt !== sessionStartedAt) {
460
+ state3.attempts += 1;
461
+ state3.lastAttemptedSessionStartedAt = sessionStartedAt;
455
462
  }
456
- presenceReaperState.set(sk, state2);
457
- if (state2.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
463
+ presenceReaperState.set(sk, state3);
464
+ if (state3.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
458
465
  givenUp.push(key);
459
- if (!state2.gaveUpLogged) {
466
+ if (!state3.gaveUpLogged) {
460
467
  log2(
461
468
  `[mcp-presence-reaper] giving up on '${codeName}:${key}' after ${MAX_PRESENCE_RESTART_ATTEMPTS} consecutive failed restarts \u2014 declared but never recovers (likely orphaned config; see ENG-5279)`
462
469
  );
463
- state2.gaveUpLogged = true;
470
+ state3.gaveUpLogged = true;
464
471
  if (onGiveUp) {
465
472
  try {
466
473
  onGiveUp(codeName, key);
@@ -541,6 +548,61 @@ function decideChannelRestart(input) {
541
548
  return { restart: true, added, removed };
542
549
  }
543
550
 
551
+ // src/lib/usage-banner-monitor.ts
552
+ var MIN_CHECK_INTERVAL_MS = 6e4;
553
+ var PANE_TAIL_LINES_FOR_BANNER = 200;
554
+ var state = /* @__PURE__ */ new Map();
555
+ async function maybeReportUsageBanner(args) {
556
+ const { api: api2, codeName, agentId, log: log2 } = args;
557
+ const now = args.now ?? /* @__PURE__ */ new Date();
558
+ const nowMs = now.getTime();
559
+ const existing = state.get(codeName);
560
+ if (existing && nowMs - existing.lastCheckedAt < MIN_CHECK_INTERVAL_MS) {
561
+ return;
562
+ }
563
+ let tail;
564
+ try {
565
+ tail = readPaneLogTail(codeName, PANE_TAIL_LINES_FOR_BANNER);
566
+ } catch (err) {
567
+ log2(`[usage-banner] readPaneLogTail failed for '${codeName}': ${err.message}`);
568
+ return;
569
+ }
570
+ const next = {
571
+ lastPct: existing?.lastPct ?? null,
572
+ lastWeekResetsAt: existing?.lastWeekResetsAt ?? null,
573
+ lastCheckedAt: nowMs
574
+ };
575
+ if (!tail) {
576
+ state.set(codeName, next);
577
+ return;
578
+ }
579
+ const observation = parseUsageBanner(tail, now);
580
+ if (!observation) {
581
+ state.set(codeName, next);
582
+ return;
583
+ }
584
+ const observedAtIso = next.lastWeekResetsAt;
585
+ const weekResetsAtIso = observation.weekResetsAt.toISOString();
586
+ if (existing && existing.lastPct === observation.pct && observedAtIso === weekResetsAtIso) {
587
+ state.set(codeName, next);
588
+ return;
589
+ }
590
+ try {
591
+ await api2.post("/host/usage-observations", {
592
+ agent_id: agentId,
593
+ observed_pct: observation.pct,
594
+ week_resets_at: weekResetsAtIso,
595
+ source: "pane_log"
596
+ });
597
+ next.lastPct = observation.pct;
598
+ next.lastWeekResetsAt = weekResetsAtIso;
599
+ state.set(codeName, next);
600
+ } catch (err) {
601
+ log2(`[usage-banner] POST /host/usage-observations failed for '${codeName}': ${err.message}`);
602
+ state.set(codeName, next);
603
+ }
604
+ }
605
+
544
606
  // src/lib/poll-backoff.ts
545
607
  var POLL_BACKOFF_BASE_MS = 1e3;
546
608
  var POLL_BACKOFF_MAX_MS = 6e4;
@@ -2213,8 +2275,8 @@ var KNOWN_SAFE_TAIL_SIGNATURES = /* @__PURE__ */ new Set(["session_id_in_use"]);
2213
2275
  function shouldSkipRevokedCleanup(previousKnownStatus) {
2214
2276
  return previousKnownStatus === "revoked";
2215
2277
  }
2216
- function hasRevokedResiduals(state2) {
2217
- return state2.gatewayRunning || state2.portAllocated || state2.provisionDirExists;
2278
+ function hasRevokedResiduals(state3) {
2279
+ return state3.gatewayRunning || state3.portAllocated || state3.provisionDirExists;
2218
2280
  }
2219
2281
  var knownVersions = /* @__PURE__ */ new Map();
2220
2282
  var knownStatuses = /* @__PURE__ */ new Map();
@@ -2334,8 +2396,14 @@ function managedToolkitMapKey(agentId, serverId) {
2334
2396
  var lastCronRunTs = /* @__PURE__ */ new Map();
2335
2397
  var lastWorkTriggerAt = /* @__PURE__ */ new Map();
2336
2398
  var alertedStaleItems = /* @__PURE__ */ new Set();
2399
+ var stuckKanbanLogged = /* @__PURE__ */ new Set();
2337
2400
  var apiKeyStatusCache = /* @__PURE__ */ new Map();
2338
- var STALE_TASK_THRESHOLD_MS = 30 * 60 * 1e3;
2401
+ var STALE_TASK_THRESHOLD_MS = (() => {
2402
+ const env = process.env["AUGMENTED_STUCK_TASK_THRESHOLD_MINUTES"];
2403
+ const minutes = env ? Number.parseInt(env, 10) : NaN;
2404
+ if (Number.isFinite(minutes) && minutes > 0) return minutes * 60 * 1e3;
2405
+ return 30 * 60 * 1e3;
2406
+ })();
2339
2407
  var alertSlackWebhook = null;
2340
2408
  var alertedJobs = /* @__PURE__ */ new Set();
2341
2409
  var taskDisplayInfo = /* @__PURE__ */ new Map();
@@ -2344,7 +2412,7 @@ var codeNameToAgentId = /* @__PURE__ */ new Map();
2344
2412
  var agentChannelTokens = /* @__PURE__ */ new Map();
2345
2413
  var activeChannels = /* @__PURE__ */ new Map();
2346
2414
  var gatewaysStartedThisCycle = /* @__PURE__ */ new Set();
2347
- var state = {
2415
+ var state2 = {
2348
2416
  pid: process.pid,
2349
2417
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2350
2418
  lastPollAt: null,
@@ -2399,7 +2467,8 @@ function clearAgentCaches(agentId, codeName) {
2399
2467
  var cachedFrameworkVersion = null;
2400
2468
  var lastVersionCheckAt = 0;
2401
2469
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
2402
- var agtCliVersion = true ? "0.21.8" : "dev";
2470
+ var lastResponsivenessProbeAt = 0;
2471
+ var agtCliVersion = true ? "0.22.0" : "dev";
2403
2472
  function resolveBrewPath(execFileSync4) {
2404
2473
  try {
2405
2474
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -3141,10 +3210,10 @@ function isGatewayHung(codeName) {
3141
3210
  if (!Array.isArray(jobs)) return false;
3142
3211
  const now = Date.now();
3143
3212
  for (const job of jobs) {
3144
- const state2 = job.state;
3145
- if (!state2) continue;
3146
- const runStartedAt = state2.runStartedAtMs;
3147
- const isRunning = state2.status === "running" || state2.running === true;
3213
+ const state3 = job.state;
3214
+ if (!state3) continue;
3215
+ const runStartedAt = state3.runStartedAtMs;
3216
+ const isRunning = state3.status === "running" || state3.running === true;
3148
3217
  if (isRunning && runStartedAt && now - runStartedAt > GATEWAY_HUNG_TIMEOUT_MS) {
3149
3218
  return true;
3150
3219
  }
@@ -3345,7 +3414,7 @@ async function pollCycle() {
3345
3414
  const now = Date.now();
3346
3415
  if (now - lastVersionCheckAt > VERSION_CHECK_INTERVAL_MS) {
3347
3416
  try {
3348
- const firstAgent = state.agents[0];
3417
+ const firstAgent = state2.agents[0];
3349
3418
  const versionAdapter = firstAgent ? resolveAgentFramework(firstAgent.codeName) : getFramework("openclaw");
3350
3419
  if (versionAdapter.getVersion) {
3351
3420
  cachedFrameworkVersion = await versionAdapter.getVersion();
@@ -3356,7 +3425,7 @@ async function pollCycle() {
3356
3425
  }
3357
3426
  try {
3358
3427
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
3359
- const { collectDiagnostics } = await import("../persistent-session-F44544YP.js");
3428
+ const { collectDiagnostics } = await import("../persistent-session-EI7576OO.js");
3360
3429
  const diagCodeNames = [...persistentSessionAgents];
3361
3430
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
3362
3431
  let tailscaleHostname;
@@ -3403,13 +3472,34 @@ async function pollCycle() {
3403
3472
  } catch (err) {
3404
3473
  log(`Heartbeat failed: ${err.message}`);
3405
3474
  }
3475
+ try {
3476
+ const {
3477
+ collectResponsivenessProbes,
3478
+ getResponsivenessIntervalMs
3479
+ } = await import("../responsiveness-probe-SIMNGYC4.js");
3480
+ const probeIntervalMs = getResponsivenessIntervalMs();
3481
+ if (now - lastResponsivenessProbeAt > probeIntervalMs) {
3482
+ const probeCodeNames = [...persistentSessionAgents];
3483
+ if (probeCodeNames.length > 0) {
3484
+ const probes = collectResponsivenessProbes(probeCodeNames);
3485
+ if (probes.length > 0) {
3486
+ void api.post("/host/responsiveness-probe", { host_id: hostId, probes }).catch((err) => {
3487
+ log(`[responsiveness-probe] post failed: ${err.message}`);
3488
+ });
3489
+ }
3490
+ }
3491
+ lastResponsivenessProbeAt = now;
3492
+ }
3493
+ } catch (err) {
3494
+ log(`[responsiveness-probe] collection failed: ${err.message}`);
3495
+ }
3406
3496
  const data = await api.post("/host/agents", { host_id: hostId });
3407
3497
  const agents = data.agents ?? [];
3408
3498
  const restartAcks = /* @__PURE__ */ new Map();
3409
3499
  for (const agent of agents) {
3410
3500
  const requested = agent.restart_requested_at ?? null;
3411
3501
  if (!requested) continue;
3412
- const prev = state.agents.find((a) => a.agentId === agent.agent_id);
3502
+ const prev = state2.agents.find((a) => a.agentId === agent.agent_id);
3413
3503
  const lastProcessed = prev?.lastRestartProcessedAt ?? null;
3414
3504
  if (lastProcessed && Date.parse(lastProcessed) >= Date.parse(requested)) continue;
3415
3505
  log(`[restart] Dashboard requested restart for '${agent.code_name}' at ${requested}`);
@@ -3432,7 +3522,7 @@ async function pollCycle() {
3432
3522
  await processAgent(agent, agentStates);
3433
3523
  } catch (err) {
3434
3524
  log(`Error processing agent '${agent.code_name}': ${err.message}`);
3435
- const existing = state.agents.find((a) => a.agentId === agent.agent_id);
3525
+ const existing = state2.agents.find((a) => a.agentId === agent.agent_id);
3436
3526
  if (existing) {
3437
3527
  agentStates.push(existing);
3438
3528
  } else {
@@ -3457,12 +3547,12 @@ async function pollCycle() {
3457
3547
  }
3458
3548
  const restartAckStateChanged = applyRestartAcks({
3459
3549
  agentStates,
3460
- priorAgents: state.agents,
3550
+ priorAgents: state2.agents,
3461
3551
  restartAcks
3462
3552
  });
3463
3553
  if (restartAckStateChanged) {
3464
3554
  try {
3465
- const ackedState = { ...state, agents: agentStates };
3555
+ const ackedState = { ...state2, agents: agentStates };
3466
3556
  writeFileSync3(getStateFile(), JSON.stringify(ackedState, null, 2));
3467
3557
  } catch (err) {
3468
3558
  log(`[restart] failed to persist ack immediately: ${err.message}`);
@@ -3480,7 +3570,7 @@ async function pollCycle() {
3480
3570
  } catch {
3481
3571
  }
3482
3572
  const currentIds = new Set(agents.map((a) => a.agent_id));
3483
- for (const prev of state.agents) {
3573
+ for (const prev of state2.agents) {
3484
3574
  if (!currentIds.has(prev.agentId)) {
3485
3575
  log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
3486
3576
  const adapter = resolveAgentFramework(prev.codeName);
@@ -3580,19 +3670,19 @@ async function pollCycle() {
3580
3670
  }
3581
3671
  } catch {
3582
3672
  }
3583
- state = {
3584
- ...state,
3673
+ state2 = {
3674
+ ...state2,
3585
3675
  lastPollAt: (/* @__PURE__ */ new Date()).toISOString(),
3586
- pollCount: state.pollCount + 1,
3676
+ pollCount: state2.pollCount + 1,
3587
3677
  agents: agentStates
3588
3678
  };
3589
3679
  if (consecutivePollFailures > 0) {
3590
3680
  log(`[poll-backoff] recovered after ${consecutivePollFailures} failure(s), resuming normal interval`);
3591
3681
  consecutivePollFailures = 0;
3592
3682
  }
3593
- send({ type: "state-update", state });
3683
+ send({ type: "state-update", state: state2 });
3594
3684
  } catch (err) {
3595
- state.errorCount++;
3685
+ state2.errorCount++;
3596
3686
  const message = err.message;
3597
3687
  log(`Poll error: ${message}`);
3598
3688
  send({ type: "error", message });
@@ -3622,6 +3712,14 @@ async function processAgent(agent, agentStates) {
3622
3712
  if (agent.framework) {
3623
3713
  agentFrameworkCache.set(agent.code_name, agent.framework);
3624
3714
  }
3715
+ if (agent.framework === "claude-code" && agent.status === "active") {
3716
+ void maybeReportUsageBanner({
3717
+ api,
3718
+ codeName: agent.code_name,
3719
+ agentId: agent.agent_id,
3720
+ log
3721
+ });
3722
+ }
3625
3723
  const now = (/* @__PURE__ */ new Date()).toISOString();
3626
3724
  const adapter = resolveAgentFramework(agent.code_name);
3627
3725
  let agentDir = join4(adapter.getAgentDir(agent.code_name), "provision");
@@ -3740,7 +3838,7 @@ async function processAgent(agent, agentStates) {
3740
3838
  });
3741
3839
  } catch (err) {
3742
3840
  log(`Refresh failed for '${agent.code_name}': ${err.message}`);
3743
- const existing = state.agents.find((a) => a.agentId === agent.agent_id);
3841
+ const existing = state2.agents.find((a) => a.agentId === agent.agent_id);
3744
3842
  agentStates.push(existing ?? {
3745
3843
  agentId: agent.agent_id,
3746
3844
  codeName: agent.code_name,
@@ -3796,7 +3894,7 @@ async function processAgent(agent, agentStates) {
3796
3894
  const charterVersion = refreshData.charter.version;
3797
3895
  const toolsVersion = refreshData.tools.version;
3798
3896
  const known = knownVersions.get(agent.agent_id);
3799
- let lastProvisionAt = state.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
3897
+ let lastProvisionAt = state2.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
3800
3898
  const currentChannelIds = launchableChannelIds(refreshData.channel_configs);
3801
3899
  const previousChannelIds = knownChannels.get(agent.agent_id);
3802
3900
  const channelsChanged = !previousChannelIds || currentChannelIds.size !== previousChannelIds.size || [...currentChannelIds].some((ch) => !previousChannelIds.has(ch)) || [...previousChannelIds].some((ch) => !currentChannelIds.has(ch));
@@ -4160,7 +4258,7 @@ async function processAgent(agent, agentStates) {
4160
4258
  log(`Failed to provision direct-chat channel for '${agent.code_name}': ${err.message}`);
4161
4259
  }
4162
4260
  }
4163
- let lastSecretsProvisionAt = state.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
4261
+ let lastSecretsProvisionAt = state2.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
4164
4262
  let secretsHash = knownSecretsHashes.get(agent.agent_id) ?? null;
4165
4263
  try {
4166
4264
  const secretsData = await api.post("/host/secrets", { agent_id: agent.agent_id });
@@ -4604,6 +4702,10 @@ async function processAgent(agent, agentStates) {
4604
4702
  sessionHealthy: psResult.sessionHealthyAfter,
4605
4703
  spawnAttempted: psResult.spawnAttempted
4606
4704
  });
4705
+ if (psResult.sessionHealthyAfter) {
4706
+ ensureKanbanLoopArmed(agent.code_name, log).catch(() => {
4707
+ });
4708
+ }
4607
4709
  if (stuck.shouldWarn) {
4608
4710
  log(
4609
4711
  `[persistent-session-stuck] WARN: agent=${agent.code_name} unhealthy with no spawn attempt for ${stuck.consecutiveTicks} consecutive ticks (last_decision=${psResult.decision}) \u2014 manager-worker may need restart, or upstream guard is mis-firing (see ENG-5116)`
@@ -4612,7 +4714,7 @@ async function processAgent(agent, agentStates) {
4612
4714
  log(`[persistent-session-stuck] agent=${agent.code_name} recovered from stuck state`);
4613
4715
  }
4614
4716
  try {
4615
- checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir2(agent.code_name));
4717
+ checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
4616
4718
  } catch (err) {
4617
4719
  log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
4618
4720
  }
@@ -4620,7 +4722,7 @@ async function processAgent(agent, agentStates) {
4620
4722
  const sess = getSessionState(agent.code_name);
4621
4723
  let mcpJsonParsed = null;
4622
4724
  try {
4623
- const mcpPath = join4(getProjectDir2(agent.code_name), ".mcp.json");
4725
+ const mcpPath = join4(getProjectDir(agent.code_name), ".mcp.json");
4624
4726
  mcpJsonParsed = JSON.parse(readFileSync3(mcpPath, "utf-8"));
4625
4727
  } catch {
4626
4728
  }
@@ -4784,8 +4886,8 @@ async function processAgent(agent, agentStates) {
4784
4886
  if (sessionMode === "persistent") {
4785
4887
  if (isSessionHealthy(agent.code_name)) {
4786
4888
  const todayItem = boardItems.find((b) => b.status === "todo");
4787
- const taskHint = todayItem ? ` Top item: "${todayItem.title}" (priority ${todayItem.priority}).` : "";
4788
- injectMessage(agent.code_name, "task", `New work triggered. Check your kanban board with kanban_list and pick up the next item.${taskHint}`, {
4889
+ const triggerMessage = todayItem ? `New work triggered. Top item: id=${todayItem.id} title=${JSON.stringify(todayItem.title)} priority=${todayItem.priority}. Call kanban_move("${todayItem.id}", "in_progress") and start working \u2014 no need to kanban_list first.` : "New work triggered. Check your kanban board with kanban_list and pick up the next item.";
4890
+ injectMessage(agent.code_name, "task", triggerMessage, {
4789
4891
  task_name: "kanban-work-trigger"
4790
4892
  }, log);
4791
4893
  log(`[persistent-session] Work trigger injected for '${agent.code_name}'`);
@@ -4845,6 +4947,13 @@ ${item.title}${resultLine}`;
4845
4947
  const displayName = agentDisplayNames.get(agent.code_name) ?? agent.code_name;
4846
4948
  for (const item of staleItems) {
4847
4949
  const age = Math.round((Date.now() - new Date(item.updated_at).getTime()) / 6e4);
4950
+ const stuckKey = `${agent.code_name}:${item.id}`;
4951
+ if (!stuckKanbanLogged.has(stuckKey)) {
4952
+ log(
4953
+ `[persistent-session-stuck-kanban] agent=${agent.code_name} item_id=${item.id} title=${JSON.stringify(item.title)} status=in_progress age_minutes=${age} threshold_minutes=${Math.round(STALE_TASK_THRESHOLD_MS / 6e4)}`
4954
+ );
4955
+ stuckKanbanLogged.add(stuckKey);
4956
+ }
4848
4957
  log(`Stale task: '${item.title}' (id=${item.id}) in_progress for ${age}m \u2014 auto-failing for '${agent.code_name}'`);
4849
4958
  alertedStaleItems.add(`${agent.code_name}:${item.id}`);
4850
4959
  try {
@@ -4881,6 +4990,14 @@ In progress for ${age} minutes \u2014 auto-failed`).catch(() => {
4881
4990
  }
4882
4991
  }
4883
4992
  }
4993
+ for (const key of stuckKanbanLogged) {
4994
+ if (key.startsWith(prefix)) {
4995
+ const itemId = key.slice(prefix.length);
4996
+ if (!freshBoard.some((b) => b.id === itemId && b.status === "in_progress")) {
4997
+ stuckKanbanLogged.delete(key);
4998
+ }
4999
+ }
5000
+ }
4884
5001
  } catch (err) {
4885
5002
  log(`Board diff failed for '${agent.code_name}': ${err.message}`);
4886
5003
  }
@@ -5006,19 +5123,19 @@ function clearStaleCronRunState(jobsPath) {
5006
5123
  let changed = false;
5007
5124
  const now = Date.now();
5008
5125
  for (const job of jobs) {
5009
- const state2 = job.state;
5010
- if (!state2) continue;
5011
- const runStartedAt = state2.runningAtMs ?? state2.runStartedAtMs;
5012
- const isRunning = state2.running === true || state2.status === "running";
5126
+ const state3 = job.state;
5127
+ if (!state3) continue;
5128
+ const runStartedAt = state3.runningAtMs ?? state3.runStartedAtMs;
5129
+ const isRunning = state3.running === true || state3.status === "running";
5013
5130
  if (isRunning && runStartedAt && now - runStartedAt > STALE_RUN_TIMEOUT_MS) {
5014
- state2.running = false;
5015
- delete state2.status;
5016
- delete state2.runStartedAtMs;
5017
- delete state2.currentRunId;
5131
+ state3.running = false;
5132
+ delete state3.status;
5133
+ delete state3.runStartedAtMs;
5134
+ delete state3.currentRunId;
5018
5135
  changed = true;
5019
5136
  log(`Cleared stale running state for cron job '${job.name}' (stuck for ${Math.round((now - runStartedAt) / 6e4)}min)`);
5020
5137
  } else if (isRunning && !runStartedAt) {
5021
- state2.running = false;
5138
+ state3.running = false;
5022
5139
  changed = true;
5023
5140
  log(`Cleared stale running state for cron job '${job.name}' (no start timestamp)`);
5024
5141
  }
@@ -5082,16 +5199,16 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
5082
5199
  enabled: t.enabled ?? true,
5083
5200
  triggered_at: t.triggered_at ?? null
5084
5201
  }));
5085
- const state3 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
5086
- claudeSchedulerStates.set(codeName, state3);
5202
+ const state4 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
5203
+ claudeSchedulerStates.set(codeName, state4);
5087
5204
  knownTasksHashes.set(agent.agent_id, combinedHash);
5088
5205
  log(`[claude-scheduler] Tasks synced for '${codeName}' (${taskInputs.length} task(s))`);
5089
5206
  }
5090
5207
  if (!claudeSchedulerStates.has(codeName)) {
5091
5208
  claudeSchedulerStates.set(codeName, loadSchedulerState(codeName));
5092
5209
  }
5093
- const state2 = claudeSchedulerStates.get(codeName);
5094
- const ready = getReadyTasks(state2, inFlightClaudeTasks);
5210
+ const state3 = claudeSchedulerStates.get(codeName);
5211
+ const ready = getReadyTasks(state3, inFlightClaudeTasks);
5095
5212
  if (ready.length === 0) return;
5096
5213
  for (const task of ready) {
5097
5214
  if ((claudeTaskConcurrency.get(codeName) ?? 0) >= MAX_CLAUDE_CONCURRENCY) break;
@@ -5179,7 +5296,7 @@ async function fetchPriorScheduledRuns(agentId, taskId) {
5179
5296
  }
5180
5297
  }
5181
5298
  async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
5182
- const projectDir = getProjectDir(codeName);
5299
+ const projectDir = getProjectDir2(codeName);
5183
5300
  const mcpConfigPath = join4(projectDir, ".mcp.json");
5184
5301
  let runId = null;
5185
5302
  let kanbanItemId = null;
@@ -5372,8 +5489,8 @@ async function processClaudeTaskResult(codeName, agentId, templateId, rawOutput,
5372
5489
  }
5373
5490
  }
5374
5491
  function fireClaudeWorkTrigger(codeName, agentId, boardItems) {
5375
- const state2 = claudeSchedulerStates.get(codeName) ?? loadSchedulerState(codeName);
5376
- const kanbanTask = findTaskByTemplate(state2, "kanban-work");
5492
+ const state3 = claudeSchedulerStates.get(codeName) ?? loadSchedulerState(codeName);
5493
+ const kanbanTask = findTaskByTemplate(state3, "kanban-work");
5377
5494
  if (!kanbanTask) {
5378
5495
  log(`[claude-scheduler] Work trigger: no kanban-work task found for '${codeName}'`);
5379
5496
  return;
@@ -5400,7 +5517,7 @@ var persistentSessionStuckTracker = new PersistentSessionStuckTracker();
5400
5517
  var claudeAuthTupleBySession = /* @__PURE__ */ new Map();
5401
5518
  async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5402
5519
  const codeName = agent.code_name;
5403
- const projectDir = getProjectDir2(codeName);
5520
+ const projectDir = getProjectDir(codeName);
5404
5521
  const mcpConfigPath = join4(projectDir, ".mcp.json");
5405
5522
  const claudeMdPath = join4(projectDir, "CLAUDE.md");
5406
5523
  const teamSettingsForTz = refreshData.team?.settings;
@@ -5556,6 +5673,11 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash2("sha256").
5556
5673
  } catch (err) {
5557
5674
  log(`[persistent-session] Failed to provision isolation hook for '${codeName}': ${err.message}`);
5558
5675
  }
5676
+ try {
5677
+ provisionOrientHook(codeName);
5678
+ } catch (err) {
5679
+ log(`[persistent-session] Failed to provision orient hook for '${codeName}': ${err.message}`);
5680
+ }
5559
5681
  const sessionRunResult = await startRun({
5560
5682
  agent_id: agent.agent_id,
5561
5683
  source_type: "system",
@@ -5616,13 +5738,18 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash2("sha256").
5616
5738
  } else if (!claudeSchedulerStates.has(codeName)) {
5617
5739
  claudeSchedulerStates.set(codeName, loadSchedulerState(codeName));
5618
5740
  }
5619
- const state2 = claudeSchedulerStates.get(codeName);
5620
- if (state2) {
5621
- const ready = getReadyTasks(state2, inFlightClaudeTasks);
5741
+ const state3 = claudeSchedulerStates.get(codeName);
5742
+ if (state3) {
5743
+ const ready = getReadyTasks(state3, inFlightClaudeTasks);
5622
5744
  if (ready.length > 0) {
5623
5745
  log(`[persistent-session] ${ready.length} ready task(s) for '${codeName}': ${ready.map((t) => `${t.name}(next=${t.nextFireAt ? new Date(t.nextFireAt).toISOString() : "null"})`).join(", ")}`);
5624
5746
  }
5625
5747
  for (const task of ready) {
5748
+ if (KANBAN_WORK_TEMPLATES.has(task.templateId) && isKanbanWorkCronDisabled() && isKanbanLoopArmedForCurrentSession(codeName)) {
5749
+ const updated = markTaskFired(codeName, task.taskId, "ok");
5750
+ claudeSchedulerStates.set(codeName, updated);
5751
+ continue;
5752
+ }
5626
5753
  if (KANBAN_WORK_TEMPLATES.has(task.templateId) && !hasActionableItems(boardItems)) {
5627
5754
  log(`[persistent-session] Skipping '${task.name}' for '${codeName}' \u2014 board is empty`);
5628
5755
  const updated = markTaskFired(codeName, task.taskId, "ok");
@@ -5845,7 +5972,7 @@ function ensureRealtimeKanbanStarted(agentStates) {
5845
5972
  if (agentFw === "claude-code") {
5846
5973
  const boardItems = kanbanBoardCache.get(agent.codeName) ?? [];
5847
5974
  if (isSessionHealthy(agent.codeName)) {
5848
- injectMessage(agent.codeName, "task", `New task added to your board: "${item.title}" (priority ${item.priority}). Pick it up \u2014 move to in_progress and start working.`, {
5975
+ injectMessage(agent.codeName, "task", `New task added to your board: id=${item.id} title=${JSON.stringify(item.title)} priority=${item.priority}. Call kanban_move("${item.id}", "in_progress") and start working.`, {
5849
5976
  task_name: "kanban-work-trigger"
5850
5977
  }, log);
5851
5978
  log(`[realtime] Injected kanban-work trigger for '${agent.codeName}': "${item.title}"`);
@@ -6084,6 +6211,10 @@ var STANDUP_TEMPLATES = /* @__PURE__ */ new Set(["daily-standup", "end-of-day-su
6084
6211
  var TASK_UPDATE_TEMPLATES = /* @__PURE__ */ new Set(["hourly-status", "task-update"]);
6085
6212
  var PLAN_TEMPLATES = /* @__PURE__ */ new Set(["morning-plan"]);
6086
6213
  var KANBAN_WORK_TEMPLATES = /* @__PURE__ */ new Set(["kanban-work"]);
6214
+ function isKanbanWorkCronDisabled() {
6215
+ const v = process.env["AGT_DISABLE_KANBAN_WORK_CRON"];
6216
+ return v === "1" || v?.toLowerCase() === "true";
6217
+ }
6087
6218
  var BOARD_INJECT_TEMPLATES = /* @__PURE__ */ new Set(["morning-plan", "task-update", "hourly-status", "end-of-day-summary", "kanban-work"]);
6088
6219
  var ACTIONABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo", "in_progress"]);
6089
6220
  function hasActionableItems(items) {
@@ -6922,7 +7053,7 @@ async function processClaudePairSessions(agents) {
6922
7053
  killPairSession,
6923
7054
  pairTmuxSession,
6924
7055
  finalizeClaudePairOnboarding
6925
- } = await import("../claude-pair-runtime-ZMTHMKMT.js");
7056
+ } = await import("../claude-pair-runtime-LZAGWNME.js");
6926
7057
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
6927
7058
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
6928
7059
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -7497,8 +7628,8 @@ function startManager(opts) {
7497
7628
  const raw = readFileSync3(stateFile, "utf-8");
7498
7629
  const parsed = JSON.parse(raw);
7499
7630
  if (Array.isArray(parsed.agents)) {
7500
- state.agents = parsed.agents;
7501
- log(`[startup] rehydrated ${state.agents.length} agent state(s) from ${stateFile}`);
7631
+ state2.agents = parsed.agents;
7632
+ log(`[startup] rehydrated ${state2.agents.length} agent state(s) from ${stateFile}`);
7502
7633
  }
7503
7634
  }
7504
7635
  } catch (err) {
@@ -7573,7 +7704,7 @@ function deployMcpAssets() {
7573
7704
  let mcpSourceDir = "";
7574
7705
  let dir = moduleDir;
7575
7706
  for (let i = 0; i < 6; i++) {
7576
- const candidate = join4(dir, "mcp");
7707
+ const candidate = join4(dir, "dist", "mcp");
7577
7708
  if (existsSync3(join4(candidate, "index.js"))) {
7578
7709
  mcpSourceDir = candidate;
7579
7710
  break;
@@ -7678,6 +7809,7 @@ export {
7678
7809
  extractCharterSlackPeers,
7679
7810
  extractCharterTelegramPeers,
7680
7811
  hasRevokedResiduals,
7812
+ isKanbanWorkCronDisabled,
7681
7813
  markAgentForFreshMemorySync,
7682
7814
  maybeUpgradeClaudeCode,
7683
7815
  shouldSkipRevokedCleanup,