@integrity-labs/agt-cli 0.28.1170 → 0.28.1172

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.
package/dist/bin/agt.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-6UXQEE6G.js";
43
+ } from "../chunk-7XUOPTSD.js";
44
44
  import {
45
45
  getProjectDir,
46
46
  isSessionResumeDisabled,
@@ -5513,7 +5513,7 @@ import { execFileSync, execSync } from "child_process";
5513
5513
  import { existsSync as existsSync11, realpathSync as realpathSync2 } from "fs";
5514
5514
  import chalk18 from "chalk";
5515
5515
  import ora16 from "ora";
5516
- var cliVersion = true ? "0.28.1170" : "dev";
5516
+ var cliVersion = true ? "0.28.1172" : "dev";
5517
5517
  async function fetchLatestVersion() {
5518
5518
  const host2 = getHost();
5519
5519
  if (!host2) return null;
@@ -6773,7 +6773,7 @@ function handleError(err) {
6773
6773
  }
6774
6774
 
6775
6775
  // src/bin/agt.ts
6776
- var cliVersion2 = true ? "0.28.1170" : "dev";
6776
+ var cliVersion2 = true ? "0.28.1172" : "dev";
6777
6777
  var program = new Command();
6778
6778
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
6779
6779
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -8078,7 +8078,7 @@ function exchangeFailureKind(err) {
8078
8078
  }
8079
8079
 
8080
8080
  // src/lib/api-client.ts
8081
- var agtCliVersion = true ? "0.28.1170" : "dev";
8081
+ var agtCliVersion = true ? "0.28.1172" : "dev";
8082
8082
  var lastConfigHash = null;
8083
8083
  function setConfigHash(hash) {
8084
8084
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -13235,4 +13235,4 @@ export {
13235
13235
  managerInstallSystemUnitCommand,
13236
13236
  managerUninstallSystemUnitCommand
13237
13237
  };
13238
- //# sourceMappingURL=chunk-6UXQEE6G.js.map
13238
+ //# sourceMappingURL=chunk-7XUOPTSD.js.map
@@ -77,7 +77,7 @@ import {
77
77
  setConfigHash,
78
78
  shellGuard,
79
79
  tripClass
80
- } from "../chunk-6UXQEE6G.js";
80
+ } from "../chunk-7XUOPTSD.js";
81
81
  import {
82
82
  CLAUDE_CODE_SECURITY_FLOOR,
83
83
  SELF_UPDATE_TIMEOUTS,
@@ -970,7 +970,9 @@ function claudeCodeUpgradeThrottled() {
970
970
 
971
971
  // src/lib/claude-pair-loop.ts
972
972
  var PAIR_LOOP_ACTIVE_INTERVAL_MS = 2e3;
973
- var PAIR_LOOP_IDLE_INTERVAL_MS = 1e4;
973
+ var PAIR_LOOP_IDLE_INTERVAL_MS = 3e4;
974
+ var PAIR_LOOP_FIRST_TICK_MS = 1e4;
975
+ var PAIR_LOOP_FAILURE_BASE_MS = 1e4;
974
976
  var PAIR_LOOP_MAX_BACKOFF_MS = 12e4;
975
977
  var PAIR_LOOP_ARMED_MAX_MS = 15 * 6e4;
976
978
  function pairLoopArmed(opts) {
@@ -993,7 +995,8 @@ function pairLoopArmed(opts) {
993
995
  function nextPairLoopDelayMs(armed, consecutiveFailures) {
994
996
  if (consecutiveFailures > 0) {
995
997
  const exp = Math.min(consecutiveFailures - 1, 16);
996
- return Math.min(PAIR_LOOP_IDLE_INTERVAL_MS * 2 ** exp, PAIR_LOOP_MAX_BACKOFF_MS);
998
+ const backoff = Math.min(PAIR_LOOP_FAILURE_BASE_MS * 2 ** exp, PAIR_LOOP_MAX_BACKOFF_MS);
999
+ return armed ? backoff : Math.max(backoff, PAIR_LOOP_IDLE_INTERVAL_MS);
997
1000
  }
998
1001
  return armed ? PAIR_LOOP_ACTIVE_INTERVAL_MS : PAIR_LOOP_IDLE_INTERVAL_MS;
999
1002
  }
@@ -1013,11 +1016,11 @@ var ClaudePairLoop = class {
1013
1016
  inFlightTick = null;
1014
1017
  setTimer;
1015
1018
  clearTimer;
1016
- /** Arm the loop. The first tick runs after one idle interval. Idempotent. */
1019
+ /** Arm the loop. The first tick runs after {@link PAIR_LOOP_FIRST_TICK_MS}. Idempotent. */
1017
1020
  start() {
1018
1021
  if (this.started) return;
1019
1022
  this.started = true;
1020
- this.schedule(PAIR_LOOP_IDLE_INTERVAL_MS);
1023
+ this.schedule(PAIR_LOOP_FIRST_TICK_MS);
1021
1024
  }
1022
1025
  /**
1023
1026
  * Disarm the loop. No new tick starts, and a callback from this generation
@@ -7452,6 +7455,21 @@ function createTickBoardFetcher(fetchBoard) {
7452
7455
  return inflight;
7453
7456
  };
7454
7457
  }
7458
+ async function fetchBoardThroughGate(opts) {
7459
+ const now = opts.now ?? Date.now;
7460
+ const token = opts.gate.beginFetch(opts.key);
7461
+ if (!opts.gate.shouldFetch(opts.key, now())) {
7462
+ const held = opts.held();
7463
+ if (held !== void 0) {
7464
+ opts.onDecision?.("gated");
7465
+ return held;
7466
+ }
7467
+ }
7468
+ const rows = await opts.fetch();
7469
+ opts.gate.markFetched(opts.key, token, now());
7470
+ opts.onDecision?.("fetched");
7471
+ return rows;
7472
+ }
7455
7473
 
7456
7474
  // src/lib/kanban-board-token.ts
7457
7475
  var BoardTokenCache = class {
@@ -7510,6 +7528,20 @@ var BoardTokenCache = class {
7510
7528
  }
7511
7529
  return rows;
7512
7530
  }
7531
+ /**
7532
+ * ENG-11720: the raw rows of the last board a successful request returned, or
7533
+ * undefined when none is held. Served by the `/host/my-kanban` fetch gate
7534
+ * while it is closed. Returns the held array itself: the one caller re-maps
7535
+ * it through `sanitizeBoardItem` on every serve, which is what keeps the
7536
+ * consumers from aliasing it (see the header).
7537
+ *
7538
+ * Counts nothing. The stats above describe what the conditional REQUEST did,
7539
+ * and a gated pass made no request — counting it as `unchanged` would report
7540
+ * a server-confirmed hit that never happened.
7541
+ */
7542
+ held(agentId) {
7543
+ return this.entries.get(agentId)?.rows;
7544
+ }
7513
7545
  /**
7514
7546
  * Drop an agent's board. Teardown must call this: a board left behind is
7515
7547
  * served to the agent if it is later RE-ADDED, which is exactly when its work
@@ -7583,6 +7615,13 @@ function createFetchGate(ttlMs) {
7583
7615
  }
7584
7616
  };
7585
7617
  }
7618
+ async function runIfDue(gate, key, fn, now = Date.now) {
7619
+ const token = gate.beginFetch(key);
7620
+ if (!gate.shouldFetch(key, now())) return { ran: false };
7621
+ const value = await fn();
7622
+ gate.markFetched(key, token, now());
7623
+ return { ran: true, value };
7624
+ }
7586
7625
  function forceGates(gates, agentId) {
7587
7626
  for (const gate of gates) {
7588
7627
  if (agentId === void 0) gate.forceAll();
@@ -13898,7 +13937,15 @@ function recordLatency(ms) {
13898
13937
  }
13899
13938
  }
13900
13939
  function startRealtimeKanban(config2) {
13901
- const { agentIds, onTodayItem, onCompletion, log: log2 } = config2;
13940
+ const { agentIds, onTodayItem, onCompletion, onBoardChanged, log: log2 } = config2;
13941
+ const boardChanged = (agentId) => {
13942
+ if (!onBoardChanged || !agentId) return;
13943
+ try {
13944
+ onBoardChanged(agentId);
13945
+ } catch (err) {
13946
+ log2(`[realtime] Kanban board-change handler error: ${err.message}`);
13947
+ }
13948
+ };
13902
13949
  if (agentIds.length === 0) return;
13903
13950
  const sb = ensureClient(config2);
13904
13951
  const filterStr = agentIds.length === 1 ? `agent_id=eq.${agentIds[0]}` : `agent_id=in.(${agentIds.join(",")})`;
@@ -13909,6 +13956,7 @@ function startRealtimeKanban(config2) {
13909
13956
  filter: filterStr
13910
13957
  }, (payload) => {
13911
13958
  const item = payload.new;
13959
+ boardChanged(item?.agent_id);
13912
13960
  if (item.status === "todo") {
13913
13961
  log2(`[realtime] New kanban item in 'todo': item_id=${item.id} agent=${item.agent_id}`);
13914
13962
  onTodayItem(item);
@@ -13921,6 +13969,7 @@ function startRealtimeKanban(config2) {
13921
13969
  }, (payload) => {
13922
13970
  const item = payload.new;
13923
13971
  const old = payload.old;
13972
+ boardChanged(item?.agent_id);
13924
13973
  if (item.status === "todo" && old.status !== "todo") {
13925
13974
  log2(`[realtime] Kanban item moved to 'todo': item_id=${item.id} agent=${item.agent_id}`);
13926
13975
  onTodayItem(item);
@@ -15830,6 +15879,7 @@ function clearAgentCaches(agentId, codeName) {
15830
15879
  pendingRecoveryCost.delete(codeName);
15831
15880
  lastRefreshPayload.delete(agentId);
15832
15881
  boardTokenCache.forget(agentId);
15882
+ boardFetchGate.forget(agentId);
15833
15883
  refreshFetchGate.forget(agentId);
15834
15884
  memoriesFetchGate.forget(agentId);
15835
15885
  secretsFetchGate.forget(agentId);
@@ -15878,7 +15928,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
15878
15928
  var lastVersionCheckAt = 0;
15879
15929
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
15880
15930
  var lastResponsivenessProbeAt = 0;
15881
- var agtCliVersion = true ? "0.28.1170" : "dev";
15931
+ var agtCliVersion = true ? "0.28.1172" : "dev";
15882
15932
  function resolveBrewPath(execFileSync3) {
15883
15933
  try {
15884
15934
  const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -17627,6 +17677,11 @@ var integrationsFetchGate = createFetchGate(DESIRED_STATE_FETCH_TTL_MS);
17627
17677
  var lastIntegrationsPayload = /* @__PURE__ */ new Map();
17628
17678
  var quarantinedFetchGate = createFetchGate(DESIRED_STATE_FETCH_TTL_MS);
17629
17679
  var lastQuarantinedPayload = /* @__PURE__ */ new Map();
17680
+ var MY_KANBAN_FETCH_TTL_MS = 3e4;
17681
+ var boardFetchGate = createFetchGate(MY_KANBAN_FETCH_TTL_MS);
17682
+ var RECURRING_SPAWN_INTERVAL_MS = 6e4;
17683
+ var RECURRING_SPAWN_GATE_KEY = "host";
17684
+ var recurringSpawnGate = createFetchGate(RECURRING_SPAWN_INTERVAL_MS);
17630
17685
  function forceIntegrationsFetch(agentId) {
17631
17686
  integrationsFetchGate.forceNext(agentId);
17632
17687
  quarantinedFetchGate.forceNext(agentId);
@@ -17639,13 +17694,20 @@ var pollCycleQuarantinedFetches = 0;
17639
17694
  var pollCycleQuarantinedSkips = 0;
17640
17695
  var pollCycleMemoriesUnchanged = 0;
17641
17696
  var pollCycleMemoriesFull = 0;
17697
+ var pollCycleBoardFetched = 0;
17698
+ var pollCycleBoardGated = 0;
17642
17699
  var DESIRED_STATE_FETCH_GATES = [
17643
17700
  refreshFetchGate,
17644
17701
  memoriesFetchGate,
17645
17702
  secretsFetchGate,
17646
17703
  defaultSchedulesFetchGate,
17647
17704
  integrationsFetchGate,
17648
- quarantinedFetchGate
17705
+ quarantinedFetchGate,
17706
+ // ENG-11720. None of the lanes reaching here changes the board (the board
17707
+ // has its own push, `onBoardChanged`), so this entry is the maintenance rule
17708
+ // above applied, not a freshness requirement. Cost: one board round trip for
17709
+ // the pushed agent, which is the pre-gate every-pass behaviour at worst.
17710
+ boardFetchGate
17649
17711
  ];
17650
17712
  function forceRefreshFetch(agentId) {
17651
17713
  forceGates(DESIRED_STATE_FETCH_GATES, agentId);
@@ -19062,6 +19124,8 @@ async function pollCycleInner() {
19062
19124
  pollCycleQuarantinedSkips = 0;
19063
19125
  pollCycleMemoriesUnchanged = 0;
19064
19126
  pollCycleMemoriesFull = 0;
19127
+ pollCycleBoardFetched = 0;
19128
+ pollCycleBoardGated = 0;
19065
19129
  boardTokenCache.resetStats();
19066
19130
  for (const agent of processOrder) {
19067
19131
  if (restartInFlight.has(agent.agent_id)) {
@@ -19103,7 +19167,7 @@ async function pollCycleInner() {
19103
19167
  }
19104
19168
  const boardStats = boardTokenCache.stats();
19105
19169
  log(
19106
- `[poll-cycle] agents=${processOrder.length} refresh_fetched=${pollCycleFetches} refresh_skipped=${pollCycleFetchSkips} memories_unchanged=${pollCycleMemoriesUnchanged} memories_full=${pollCycleMemoriesFull} board_unchanged=${boardStats.unchanged} board_full=${boardStats.full} board_refetch=${boardStats.refetch} integrations_fetched=${pollCycleIntegrationsFetches} integrations_skipped=${pollCycleIntegrationsSkips} quarantined_fetched=${pollCycleQuarantinedFetches} quarantined_skipped=${pollCycleQuarantinedSkips} duration_ms=${Date.now() - cycleStartedAt}`
19170
+ `[poll-cycle] agents=${processOrder.length} refresh_fetched=${pollCycleFetches} refresh_skipped=${pollCycleFetchSkips} memories_unchanged=${pollCycleMemoriesUnchanged} memories_full=${pollCycleMemoriesFull} board_unchanged=${boardStats.unchanged} board_full=${boardStats.full} board_refetch=${boardStats.refetch} board_fetched=${pollCycleBoardFetched} board_gated=${pollCycleBoardGated} integrations_fetched=${pollCycleIntegrationsFetches} integrations_skipped=${pollCycleIntegrationsSkips} quarantined_fetched=${pollCycleQuarantinedFetches} quarantined_skipped=${pollCycleQuarantinedSkips} duration_ms=${Date.now() - cycleStartedAt}`
19107
19171
  );
19108
19172
  void maybeReportActivityCache({ api, log });
19109
19173
  const crossAgentPrune = managedPruneLedger.summarize();
@@ -19340,9 +19404,13 @@ async function pollCycleInner() {
19340
19404
  ensureRealtimeKanbanStarted(agentStates);
19341
19405
  ensureRealtimeIntegrationContextStarted(agentStates);
19342
19406
  try {
19343
- const spawnData = await api.post("/host/kanban/recurring/spawn");
19344
- if (spawnData.spawned > 0) {
19345
- log(`Spawned ${spawnData.spawned} recurring kanban item(s)`);
19407
+ const spawn = await runIfDue(
19408
+ recurringSpawnGate,
19409
+ RECURRING_SPAWN_GATE_KEY,
19410
+ () => api.post("/host/kanban/recurring/spawn")
19411
+ );
19412
+ if (spawn.ran && spawn.value.spawned > 0) {
19413
+ log(`Spawned ${spawn.value.spawned} recurring kanban item(s)`);
19346
19414
  }
19347
19415
  } catch {
19348
19416
  }
@@ -21491,23 +21559,35 @@ async function processAgent(agent, agentStates, managedToolkits) {
21491
21559
  }
21492
21560
  }
21493
21561
  const fetchTickBoard = createTickBoardFetcher(async () => {
21494
- const knownToken = boardTokenCache.tokenFor(agent.agent_id);
21495
- const post = (token) => api.post("/host/my-kanban", {
21496
- agent_id: agent.agent_id,
21497
- // ENG-9585: this is the ONLY caller that polls (every ~10s, per agent),
21498
- // and it reads exactly `BoardItem`. `description` and `notes` are 57.8%
21499
- // of the payload and are read here by nothing — but the `kanban_list` MCP
21500
- // tool and `agt kanban` DO read them, so the route cannot simply drop
21501
- // them. Asking for the lean shape is this caller saying what it needs;
21502
- // every other caller keeps the full payload by default.
21503
- detail: "board",
21504
- ...token ? { known_token: token } : {}
21562
+ const rawRows = await fetchBoardThroughGate({
21563
+ key: agent.agent_id,
21564
+ gate: boardFetchGate,
21565
+ held: () => boardTokenCache.held(agent.agent_id),
21566
+ onDecision: (decision) => {
21567
+ if (decision === "gated") pollCycleBoardGated += 1;
21568
+ else pollCycleBoardFetched += 1;
21569
+ },
21570
+ fetch: async () => {
21571
+ const knownToken = boardTokenCache.tokenFor(agent.agent_id);
21572
+ const post = (token) => api.post("/host/my-kanban", {
21573
+ agent_id: agent.agent_id,
21574
+ // ENG-9585: this is the ONLY caller that polls (every ~10s, per agent),
21575
+ // and it reads exactly `BoardItem`. `description` and `notes` are 57.8%
21576
+ // of the payload and are read here by nothing — but the `kanban_list` MCP
21577
+ // tool and `agt kanban` DO read them, so the route cannot simply drop
21578
+ // them. Asking for the lean shape is this caller saying what it needs;
21579
+ // every other caller keeps the full payload by default.
21580
+ detail: "board",
21581
+ ...token ? { known_token: token } : {}
21582
+ });
21583
+ let rows = boardTokenCache.resolve(agent.agent_id, await post(knownToken));
21584
+ if (rows === null) {
21585
+ rows = boardTokenCache.resolve(agent.agent_id, await post(void 0)) ?? [];
21586
+ }
21587
+ return rows;
21588
+ }
21505
21589
  });
21506
- let rows = boardTokenCache.resolve(agent.agent_id, await post(knownToken));
21507
- if (rows === null) {
21508
- rows = boardTokenCache.resolve(agent.agent_id, await post(void 0)) ?? [];
21509
- }
21510
- return rows.map(sanitizeBoardItem);
21590
+ return rawRows.map(sanitizeBoardItem);
21511
21591
  });
21512
21592
  let boardItems = [];
21513
21593
  let boardItemsStale = false;
@@ -22110,6 +22190,7 @@ async function processAgent(agent, agentStates, managedToolkits) {
22110
22190
  reason: `Timed out \u2014 in progress for ${age} minutes with no update`
22111
22191
  });
22112
22192
  reapAction = reapResult.action ?? null;
22193
+ boardFetchGate.forceNext(agentId);
22113
22194
  log(
22114
22195
  `Reap result for item_id=${item.id}: updated=${reapResult.updated} action=${reapResult.action ?? reapResult.reason ?? "none"}`
22115
22196
  );
@@ -23495,6 +23576,14 @@ function ensureRealtimeKanbanStarted(agentStates) {
23495
23576
  supabaseAnonKey: exchange.supabaseAnonKey,
23496
23577
  token: exchange.token,
23497
23578
  agentIds: activeAgentIds,
23579
+ // ENG-11720: what makes the `/host/my-kanban` gate safe. Any change to one
23580
+ // of this agent's cards reopens it, so the next pass fetches — no early
23581
+ // poll, because a card change is not urgent enough to pull a whole cycle
23582
+ // forward, and forces are counters, so a burst of UPDATEs (embedding
23583
+ // writes, notes) still costs at most one fetch per pass.
23584
+ onBoardChanged: (agentId) => {
23585
+ boardFetchGate.forceNext(agentId);
23586
+ },
23498
23587
  onTodayItem: (item) => {
23499
23588
  if (item.source_type === "scheduled_task") return;
23500
23589
  const agent = agentStates.find((a) => a.agentId === item.agent_id);