codeam-cli 2.64.0 → 2.65.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 (3) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.js +841 -342
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2791,6 +2791,15 @@ function resolveApiBaseUrl() {
2791
2791
 
2792
2792
  // ../../packages/shared/src/types/agent-squad.ts
2793
2793
  var HANDOFF_FENCE_TAG = "codeam-handoff";
2794
+ var SQUAD_CONFIGURE_COMMAND = "squad_configure";
2795
+ var SQUAD_STATS_COMMAND = "squad_stats";
2796
+ var SQUAD_HOP_BUDGET_DEFAULT = 3;
2797
+ var SQUAD_HOP_BUDGET_MIN = 1;
2798
+ var SQUAD_HOP_BUDGET_MAX = 10;
2799
+ function clampHopBudget(value) {
2800
+ if (typeof value !== "number" || !Number.isFinite(value)) return SQUAD_HOP_BUDGET_DEFAULT;
2801
+ return Math.min(SQUAD_HOP_BUDGET_MAX, Math.max(SQUAD_HOP_BUDGET_MIN, Math.round(value)));
2802
+ }
2794
2803
  var SQUAD_SPECIALTIES = {
2795
2804
  claude: "deep reasoning, refactors, and multi-step architecture work",
2796
2805
  codex: "fast, focused implementation and test fixing",
@@ -3257,6 +3266,22 @@ function makeConfig(baseDir) {
3257
3266
  s.agent = agent;
3258
3267
  save(c2);
3259
3268
  }
3269
+ function setSquadAuto2(pluginId, value) {
3270
+ const stored = {
3271
+ enabled: value.enabled === true,
3272
+ hopBudget: clampHopBudget(value.hopBudget)
3273
+ };
3274
+ const c2 = load();
3275
+ const s = c2.sessions.find((x) => x.pluginId === pluginId);
3276
+ if (!s) return null;
3277
+ s.squadAuto = stored;
3278
+ save(c2);
3279
+ return stored;
3280
+ }
3281
+ function getSquadAuto2(pluginId) {
3282
+ const s = load().sessions.find((x) => x.pluginId === pluginId);
3283
+ return s?.squadAuto ?? null;
3284
+ }
3260
3285
  function clearAll2() {
3261
3286
  try {
3262
3287
  fs3.unlinkSync(file);
@@ -3269,7 +3294,7 @@ function makeConfig(baseDir) {
3269
3294
  function loadCliConfig2() {
3270
3295
  return load();
3271
3296
  }
3272
- return { getConfig: getConfig2, ensurePluginId: ensurePluginId2, addSession: addSession2, removeSession: removeSession2, setActiveSession: setActiveSession2, getActiveSession: getActiveSession2, getActiveSessionForAgent: getActiveSessionForAgent2, setDisable1mContext: setDisable1mContext2, setSessionAgent: setSessionAgent2, clearAll: clearAll2, saveCliConfig: saveCliConfig2, loadCliConfig: loadCliConfig2 };
3297
+ return { getConfig: getConfig2, ensurePluginId: ensurePluginId2, addSession: addSession2, removeSession: removeSession2, setActiveSession: setActiveSession2, getActiveSession: getActiveSession2, getActiveSessionForAgent: getActiveSessionForAgent2, setDisable1mContext: setDisable1mContext2, setSessionAgent: setSessionAgent2, setSquadAuto: setSquadAuto2, getSquadAuto: getSquadAuto2, clearAll: clearAll2, saveCliConfig: saveCliConfig2, loadCliConfig: loadCliConfig2 };
3273
3298
  }
3274
3299
  var CODESPACE_ENV_KEYS = [
3275
3300
  "PREVIEW_TUNNEL_TOKEN",
@@ -3293,7 +3318,7 @@ function loadCodespaceEnv() {
3293
3318
  }
3294
3319
  }
3295
3320
  var _default = makeConfig();
3296
- var { getConfig, ensurePluginId, addSession, removeSession, setActiveSession, getActiveSession, getActiveSessionForAgent, setDisable1mContext, setSessionAgent, clearAll, saveCliConfig, loadCliConfig } = _default;
3321
+ var { getConfig, ensurePluginId, addSession, removeSession, setActiveSession, getActiveSession, getActiveSessionForAgent, setDisable1mContext, setSessionAgent, setSquadAuto, getSquadAuto, clearAll, saveCliConfig, loadCliConfig } = _default;
3297
3322
 
3298
3323
  // src/commands/pair-auto.ts
3299
3324
  var fs63 = __toESM(require("fs"));
@@ -8048,7 +8073,7 @@ function readAnonId() {
8048
8073
  }
8049
8074
  function superProperties() {
8050
8075
  return {
8051
- cliVersion: true ? "2.64.0" : "0.0.0-dev",
8076
+ cliVersion: true ? "2.65.0" : "0.0.0-dev",
8052
8077
  nodeVersion: process.version,
8053
8078
  platform: process.platform,
8054
8079
  arch: process.arch,
@@ -8229,7 +8254,7 @@ var os4 = __toESM(require("os"));
8229
8254
  // package.json
8230
8255
  var package_default = {
8231
8256
  name: "codeam-cli",
8232
- version: "2.64.0",
8257
+ version: "2.65.0",
8233
8258
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
8234
8259
  type: "commonjs",
8235
8260
  main: "dist/index.js",
@@ -9681,7 +9706,7 @@ var CommandRelayService = class _CommandRelayService {
9681
9706
  // fresh + clear the "CLI update available" banner after a self-update
9682
9707
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
9683
9708
  // pair/reconnect). Older backends ignore the extra field.
9684
- ..."2.64.0" ? { ideVersion: "2.64.0" } : {}
9709
+ ..."2.65.0" ? { ideVersion: "2.65.0" } : {}
9685
9710
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
9686
9711
  }
9687
9712
  /**
@@ -21100,7 +21125,7 @@ async function autoUpgradeBeforeCriticalCommand() {
21100
21125
  if (process.env.NODE_ENV === "test") return;
21101
21126
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
21102
21127
  if (process.env.CI) return;
21103
- const current2 = true ? "2.64.0" : null;
21128
+ const current2 = true ? "2.65.0" : null;
21104
21129
  if (!current2) return;
21105
21130
  const cache = readCache();
21106
21131
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -21117,7 +21142,7 @@ function checkForUpdates() {
21117
21142
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
21118
21143
  if (process.env.CI) return;
21119
21144
  if (!process.stdout.isTTY) return;
21120
- const current2 = true ? "2.64.0" : null;
21145
+ const current2 = true ? "2.65.0" : null;
21121
21146
  if (!current2) return;
21122
21147
  const cache = readCache();
21123
21148
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -21137,7 +21162,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
21137
21162
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
21138
21163
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
21139
21164
  function currentCliVersion() {
21140
- return true ? "2.64.0" : null;
21165
+ return true ? "2.65.0" : null;
21141
21166
  }
21142
21167
  function runCmd(cmd, args2, timeoutMs) {
21143
21168
  return new Promise((resolve9) => {
@@ -28558,12 +28583,292 @@ async function resolveAcpAdapterWithRetry(agent, opts = {}) {
28558
28583
  var import_node_crypto11 = require("crypto");
28559
28584
 
28560
28585
  // src/services/history.service.ts
28561
- var fs65 = __toESM(require("fs"));
28562
- var path71 = __toESM(require("path"));
28563
- var os55 = __toESM(require("os"));
28586
+ var fs66 = __toESM(require("fs"));
28587
+ var path72 = __toESM(require("path"));
28588
+ var os56 = __toESM(require("os"));
28564
28589
  var https7 = __toESM(require("https"));
28565
28590
  var http6 = __toESM(require("http"));
28566
28591
  var import_zod2 = require("zod");
28592
+
28593
+ // src/agents/acp/squad-roster.ts
28594
+ var fs65 = __toESM(require("fs"));
28595
+ var os55 = __toESM(require("os"));
28596
+ var path71 = __toESM(require("path"));
28597
+ var PROMPT_MAX = 500;
28598
+ var REPLY_SUMMARY_MAX = 1e3;
28599
+ var PREAMBLE_MAX = 2e3;
28600
+ var BRIEFING_DEFAULT_MAX = 8e3;
28601
+ var GENERIC_SPECIALTY = "general implementation tasks";
28602
+ function clip(s, max) {
28603
+ return s.length > max ? s.slice(0, max) : s;
28604
+ }
28605
+ function defaultMember() {
28606
+ return {
28607
+ acpSessionId: null,
28608
+ provisioned: false,
28609
+ binaryVerified: false,
28610
+ lastTurnIndex: 0,
28611
+ contextTextFallback: false
28612
+ };
28613
+ }
28614
+ function journalPathFor(homeDir2, sessionId) {
28615
+ return path71.join(homeDir2, ".codeam", `squad-journal-${sessionId}.json`);
28616
+ }
28617
+ function loadJournal(journalPath) {
28618
+ try {
28619
+ const raw = JSON.parse(fs65.readFileSync(journalPath, "utf-8"));
28620
+ return Array.isArray(raw.turns) ? raw.turns : [];
28621
+ } catch {
28622
+ return [];
28623
+ }
28624
+ }
28625
+ var SquadState = class {
28626
+ /** Set by the caller after fetchSquadRoster; null until then. */
28627
+ roster = null;
28628
+ /**
28629
+ * Autonomous chained handoffs (P2-2, PRO). Seeded from the persisted
28630
+ * `SavedSession.squadAuto` at session start and rewritten by
28631
+ * `squad_configure`. Read-only to callers — mutate via {@link setAuto}, which
28632
+ * clamps the budget and re-arms the chain.
28633
+ */
28634
+ autoConfig = { enabled: false, hopBudget: SQUAD_HOP_BUDGET_DEFAULT };
28635
+ /**
28636
+ * Hops left in the CURRENT chain. Reset to the budget on every USER-initiated
28637
+ * prompt (so a user turn always interrupts and re-arms) and decremented by
28638
+ * each self-accepted handoff.
28639
+ */
28640
+ hopsLeft = 0;
28641
+ /** Lifetime handoff counters for `squad_stats` (in-memory, this process). */
28642
+ handoffCounters = { proposed: 0, accepted: 0, auto: 0 };
28643
+ journalPath;
28644
+ turns;
28645
+ members = /* @__PURE__ */ new Map();
28646
+ constructor(opts) {
28647
+ this.journalPath = journalPathFor(opts.homeDir ?? os55.homedir(), opts.sessionId);
28648
+ this.turns = loadJournal(this.journalPath);
28649
+ if (opts.auto) this.setAuto(opts.auto);
28650
+ }
28651
+ get auto() {
28652
+ return this.autoConfig;
28653
+ }
28654
+ /** Apply a new mode (clamping the budget) and re-arm the chain. */
28655
+ setAuto(value) {
28656
+ this.autoConfig = {
28657
+ enabled: value.enabled === true,
28658
+ hopBudget: clampHopBudget(value.hopBudget)
28659
+ };
28660
+ this.resetHops();
28661
+ return this.autoConfig;
28662
+ }
28663
+ hopsRemaining() {
28664
+ return this.hopsLeft;
28665
+ }
28666
+ /** Re-arm the chain — called at the start of every USER-initiated turn. */
28667
+ resetHops() {
28668
+ this.hopsLeft = this.autoConfig.enabled ? this.autoConfig.hopBudget : 0;
28669
+ }
28670
+ /** Spend one hop on a self-accepted handoff. */
28671
+ consumeHop() {
28672
+ if (this.hopsLeft > 0) this.hopsLeft -= 1;
28673
+ }
28674
+ countProposal(opts) {
28675
+ this.handoffCounters.proposed += 1;
28676
+ if (opts.auto) this.handoffCounters.auto += 1;
28677
+ }
28678
+ countAccepted() {
28679
+ this.handoffCounters.accepted += 1;
28680
+ }
28681
+ /**
28682
+ * Per-member activity for the `squad_stats` relay command, derived from the
28683
+ * journal (the same shared history the delta briefing reads) plus the
28684
+ * in-memory handoff counters. `filesTouched` counts DISTINCT paths — a member
28685
+ * that edited the same file across three turns touched ONE file.
28686
+ */
28687
+ stats() {
28688
+ const byAgent = /* @__PURE__ */ new Map();
28689
+ for (const t2 of this.turns) {
28690
+ let row = byAgent.get(t2.agentId);
28691
+ if (!row) {
28692
+ row = { turns: 0, files: /* @__PURE__ */ new Set() };
28693
+ byAgent.set(t2.agentId, row);
28694
+ }
28695
+ row.turns += 1;
28696
+ for (const f of t2.filesTouched) row.files.add(f);
28697
+ }
28698
+ const members = [...byAgent.entries()].map(([agentId, row]) => ({
28699
+ agentId,
28700
+ turns: row.turns,
28701
+ filesTouched: row.files.size
28702
+ }));
28703
+ return { members, handoffs: { ...this.handoffCounters }, sinceTurn: 1 };
28704
+ }
28705
+ /** Returns (creating on first access) the mutable per-agent state. */
28706
+ member(agentId) {
28707
+ let m = this.members.get(agentId);
28708
+ if (!m) {
28709
+ m = defaultMember();
28710
+ this.members.set(agentId, m);
28711
+ }
28712
+ return m;
28713
+ }
28714
+ /** Appends a journal entry and persists (fire-and-forget durability). */
28715
+ recordTurn(entry) {
28716
+ const turn = {
28717
+ turn: this.turns.length + 1,
28718
+ agentId: entry.agentId,
28719
+ prompt: clip(entry.prompt, PROMPT_MAX),
28720
+ replySummary: clip(entry.replySummary, REPLY_SUMMARY_MAX),
28721
+ filesTouched: entry.filesTouched
28722
+ };
28723
+ this.turns.push(turn);
28724
+ this.persist();
28725
+ }
28726
+ turnCount() {
28727
+ return this.turns.length;
28728
+ }
28729
+ entriesSince(turnIndex) {
28730
+ return this.turns.filter((t2) => t2.turn > turnIndex);
28731
+ }
28732
+ persist() {
28733
+ try {
28734
+ fs65.mkdirSync(path71.dirname(this.journalPath), { recursive: true, mode: 448 });
28735
+ fs65.writeFileSync(this.journalPath, JSON.stringify({ turns: this.turns }), { mode: 384 });
28736
+ } catch {
28737
+ }
28738
+ }
28739
+ };
28740
+ function specialtyFor(agentId) {
28741
+ return SQUAD_SPECIALTIES[agentId] ?? GENERIC_SPECIALTY;
28742
+ }
28743
+ var TEAM_PREAMBLE_MARKER = "[Team context]";
28744
+ var TEAM_PREAMBLE_LINES = [
28745
+ "[Team context] You are the active agent in a CodeAgent Mobile session where the user",
28746
+ "has a squad of agents and can pass work between them. Your available teammates:",
28747
+ "If a task clearly fits a teammate better than you, you MAY propose a handoff by ending",
28748
+ `your reply with a fenced code block tagged ${HANDOFF_FENCE_TAG} containing ONE JSON object:`,
28749
+ '{"to":"<teammate id>","reason":"<one sentence>","prompt":"<the prompt they should run>"}',
28750
+ "Propose at most one handoff per reply, only when genuinely better, and never announce",
28751
+ "the block in prose \u2014 the app renders it as a card the user can accept."
28752
+ ];
28753
+ var TEAM_PREAMBLE_BULLET_RE = /^- .+ — best at: /;
28754
+ var BRIEFING_MARKER = "[Team update]";
28755
+ var BRIEFING_HEADER = "[Team update] While you were away, other agents worked on this session:";
28756
+ var BRIEFING_FOOTER = "Continue from the CURRENT state of the working tree.";
28757
+ function buildTeamPreamble(roster, currentAgent, opts) {
28758
+ const others = roster.agents.filter((a) => a.agentId !== currentAgent);
28759
+ if (others.length === 0) return null;
28760
+ const lines = [
28761
+ TEAM_PREAMBLE_LINES[0],
28762
+ TEAM_PREAMBLE_LINES[1],
28763
+ ...others.map((a) => `- ${a.displayName} \u2014 best at: ${specialtyFor(a.agentId)}`)
28764
+ ];
28765
+ if (opts.handoffInstructions) {
28766
+ lines.push(...TEAM_PREAMBLE_LINES.slice(2));
28767
+ }
28768
+ return clip(lines.join("\n"), PREAMBLE_MAX);
28769
+ }
28770
+ function renderJournalEntry(e) {
28771
+ const filesClause = e.filesTouched.length > 0 ? ` (files: ${e.filesTouched.join(", ")})` : "";
28772
+ return `- turn ${e.turn} (${e.agentId}): ${e.prompt} \u2192 ${e.replySummary}${filesClause}`;
28773
+ }
28774
+ function buildDeltaBriefing(entries, maxChars = BRIEFING_DEFAULT_MAX) {
28775
+ if (entries.length === 0) return null;
28776
+ const header = BRIEFING_HEADER;
28777
+ const footer = BRIEFING_FOOTER;
28778
+ const envelope = header.length + 1 + footer.length + 1;
28779
+ const sorted = [...entries].sort((a, b) => a.turn - b.turn);
28780
+ const lines = [];
28781
+ let bodyLen = 0;
28782
+ for (let i = sorted.length - 1; i >= 0; i--) {
28783
+ const line = renderJournalEntry(sorted[i]);
28784
+ const addedLen = line.length + (lines.length > 0 ? 1 : 0);
28785
+ if (lines.length > 0 && envelope + bodyLen + addedLen > maxChars) break;
28786
+ lines.unshift(line);
28787
+ bodyLen += addedLen;
28788
+ }
28789
+ return [header, lines.join("\n"), footer].join("\n");
28790
+ }
28791
+
28792
+ // src/agents/acp/squad-context.ts
28793
+ var SQUAD_CONTEXT_URI = "codeam://squad-context";
28794
+ var HANDOFF_MARKER = "[Session handoff]";
28795
+ var HANDOFF_TERMINATOR = "--- End of handoff context ---";
28796
+ function buildSquadContextBlock(text) {
28797
+ return {
28798
+ type: "resource",
28799
+ resource: { uri: SQUAD_CONTEXT_URI, mimeType: "text/plain", text }
28800
+ };
28801
+ }
28802
+ function isSquadContextBlock(block) {
28803
+ return block.type === "resource" && block.resource.uri === SQUAD_CONTEXT_URI;
28804
+ }
28805
+ function looksLikeUnsupportedPromptShape(err) {
28806
+ const code = err?.code;
28807
+ if (code === -32602) return true;
28808
+ const message = err instanceof Error ? err.message : String(err ?? "");
28809
+ if (/invalid[ _]params|-32602/i.test(message)) return true;
28810
+ return /\bresource\b/i.test(message) && /unsupported|not supported/i.test(message);
28811
+ }
28812
+ function skipWhile(lines, start2, keepGoing) {
28813
+ let i = start2;
28814
+ while (i < lines.length && keepGoing(lines[i])) i++;
28815
+ return i;
28816
+ }
28817
+ function endOfTeamPreamble(lines, start2) {
28818
+ if (lines[start2] !== TEAM_PREAMBLE_LINES[0]) return -1;
28819
+ if (lines[start2 + 1] !== TEAM_PREAMBLE_LINES[1]) return -1;
28820
+ return skipWhile(
28821
+ lines,
28822
+ start2 + 2,
28823
+ (line) => TEAM_PREAMBLE_BULLET_RE.test(line) || TEAM_PREAMBLE_LINES.includes(line)
28824
+ );
28825
+ }
28826
+ function endOfTerminatedBlock(lines, start2, terminator) {
28827
+ for (let i = start2; i < lines.length; i++) {
28828
+ if (lines[i].trimEnd() === terminator) return i + 1;
28829
+ }
28830
+ return -1;
28831
+ }
28832
+ function stripSquadContext(text) {
28833
+ if (!text.includes(HANDOFF_MARKER) && !text.includes(BRIEFING_MARKER) && !text.includes(TEAM_PREAMBLE_MARKER)) {
28834
+ return text;
28835
+ }
28836
+ const lines = text.split("\n");
28837
+ const kept = [];
28838
+ let i = 0;
28839
+ let stripped = false;
28840
+ while (i < lines.length) {
28841
+ const line = lines[i];
28842
+ if (line.startsWith(HANDOFF_MARKER)) {
28843
+ const end = endOfTerminatedBlock(lines, i, HANDOFF_TERMINATOR);
28844
+ if (end !== -1) {
28845
+ i = end;
28846
+ stripped = true;
28847
+ continue;
28848
+ }
28849
+ } else if (line.startsWith(BRIEFING_MARKER)) {
28850
+ const end = endOfTerminatedBlock(lines, i, BRIEFING_FOOTER);
28851
+ if (end !== -1) {
28852
+ i = end;
28853
+ stripped = true;
28854
+ continue;
28855
+ }
28856
+ } else if (line.startsWith(TEAM_PREAMBLE_MARKER)) {
28857
+ const end = endOfTeamPreamble(lines, i);
28858
+ if (end !== -1) {
28859
+ i = end;
28860
+ stripped = true;
28861
+ continue;
28862
+ }
28863
+ }
28864
+ kept.push(line);
28865
+ i++;
28866
+ }
28867
+ if (!stripped) return text;
28868
+ return kept.join("\n").trim();
28869
+ }
28870
+
28871
+ // src/services/history.service.ts
28567
28872
  var historyRecordSchema = import_zod2.z.object({
28568
28873
  type: import_zod2.z.string().optional(),
28569
28874
  timestamp: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number()]).optional(),
@@ -28583,11 +28888,24 @@ function extractText3(content) {
28583
28888
  return "";
28584
28889
  }
28585
28890
  var CONVERSATION_BATCH_SIZE = 30;
28891
+ function scrubSquadContext(messages) {
28892
+ const out2 = [];
28893
+ for (const m of messages) {
28894
+ const text = stripSquadContext(m.text);
28895
+ if (text === m.text) {
28896
+ out2.push(m);
28897
+ continue;
28898
+ }
28899
+ if (text.length === 0) continue;
28900
+ out2.push({ ...m, text });
28901
+ }
28902
+ return out2;
28903
+ }
28586
28904
  function parseJsonl(filePath) {
28587
28905
  const messages = [];
28588
28906
  let raw;
28589
28907
  try {
28590
- raw = fs65.readFileSync(filePath, "utf8");
28908
+ raw = fs66.readFileSync(filePath, "utf8");
28591
28909
  } catch (err) {
28592
28910
  if (err.code !== "ENOENT") {
28593
28911
  log.warn("history:parseJsonl", `read failed for ${filePath}`, err);
@@ -28728,7 +29046,7 @@ var HistoryService = class _HistoryService {
28728
29046
  return this._quotaPercent === null || Date.now() - this._quotaFetchedAt > ttlMs;
28729
29047
  }
28730
29048
  get projectDir() {
28731
- return this.runtime.resolveHistoryDir(this.cwd) ?? path71.join(os55.homedir(), ".claude", "projects", encodeCwd(this.cwd));
29049
+ return this.runtime.resolveHistoryDir(this.cwd) ?? path72.join(os56.homedir(), ".claude", "projects", encodeCwd(this.cwd));
28732
29050
  }
28733
29051
  /** Set the current Claude conversation ID (extracted from /cost command or session start) */
28734
29052
  setCurrentConversationId(id) {
@@ -28740,7 +29058,7 @@ var HistoryService = class _HistoryService {
28740
29058
  /** Return the current message count in the active conversation. */
28741
29059
  getCurrentMessageCount() {
28742
29060
  if (!this.currentConversationId) return 0;
28743
- const filePath = path71.join(this.projectDir, `${this.currentConversationId}.jsonl`);
29061
+ const filePath = path72.join(this.projectDir, `${this.currentConversationId}.jsonl`);
28744
29062
  return parseJsonl(filePath).length;
28745
29063
  }
28746
29064
  /**
@@ -28751,7 +29069,7 @@ var HistoryService = class _HistoryService {
28751
29069
  const deadline = Date.now() + timeoutMs;
28752
29070
  while (Date.now() < deadline) {
28753
29071
  if (!this.currentConversationId) return null;
28754
- const filePath = path71.join(this.projectDir, `${this.currentConversationId}.jsonl`);
29072
+ const filePath = path72.join(this.projectDir, `${this.currentConversationId}.jsonl`);
28755
29073
  const messages = parseJsonl(filePath);
28756
29074
  if (messages.length > previousCount) {
28757
29075
  for (let i = messages.length - 1; i >= previousCount; i--) {
@@ -28777,16 +29095,16 @@ var HistoryService = class _HistoryService {
28777
29095
  const dir = this.projectDir;
28778
29096
  const cutoff = this.bootTimeMs - _HistoryService.BIRTHTIME_GRACE_MS;
28779
29097
  try {
28780
- const files = fs65.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => {
29098
+ const files = fs66.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => {
28781
29099
  try {
28782
- const stat3 = fs65.statSync(path71.join(dir, e.name));
29100
+ const stat3 = fs66.statSync(path72.join(dir, e.name));
28783
29101
  return { name: e.name, mtime: stat3.mtimeMs, birthtime: stat3.birthtimeMs };
28784
29102
  } catch {
28785
29103
  return { name: e.name, mtime: 0, birthtime: 0 };
28786
29104
  }
28787
29105
  }).filter((f) => f.birthtime >= cutoff).sort((a, b) => b.mtime - a.mtime);
28788
29106
  if (files.length > 0) {
28789
- this.currentConversationId = path71.basename(files[0].name, ".jsonl");
29107
+ this.currentConversationId = path72.basename(files[0].name, ".jsonl");
28790
29108
  }
28791
29109
  } catch {
28792
29110
  }
@@ -28820,13 +29138,13 @@ var HistoryService = class _HistoryService {
28820
29138
  const cutoff = this.bootTimeMs - _HistoryService.BIRTHTIME_GRACE_MS;
28821
29139
  let entries;
28822
29140
  try {
28823
- entries = fs65.readdirSync(dir, { withFileTypes: true });
29141
+ entries = fs66.readdirSync(dir, { withFileTypes: true });
28824
29142
  } catch {
28825
29143
  return null;
28826
29144
  }
28827
29145
  const files = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => {
28828
29146
  try {
28829
- const stat3 = fs65.statSync(path71.join(dir, e.name));
29147
+ const stat3 = fs66.statSync(path72.join(dir, e.name));
28830
29148
  return { name: e.name, mtime: stat3.mtimeMs, birthtime: stat3.birthtimeMs };
28831
29149
  } catch {
28832
29150
  return { name: e.name, mtime: 0, birthtime: 0 };
@@ -28835,12 +29153,12 @@ var HistoryService = class _HistoryService {
28835
29153
  if (files.length === 0) return null;
28836
29154
  const targetFile = this.currentConversationId ? `${this.currentConversationId}.jsonl` : files[0].name;
28837
29155
  if (!files.some((f) => f.name === targetFile)) return null;
28838
- return this.extractUsageFromFile(path71.join(dir, targetFile));
29156
+ return this.extractUsageFromFile(path72.join(dir, targetFile));
28839
29157
  }
28840
29158
  extractUsageFromFile(filePath) {
28841
29159
  let raw;
28842
29160
  try {
28843
- raw = fs65.readFileSync(filePath, "utf8");
29161
+ raw = fs66.readFileSync(filePath, "utf8");
28844
29162
  } catch {
28845
29163
  return null;
28846
29164
  }
@@ -28885,9 +29203,9 @@ var HistoryService = class _HistoryService {
28885
29203
  let totalCost = 0;
28886
29204
  let files;
28887
29205
  try {
28888
- files = fs65.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).filter((f) => {
29206
+ files = fs66.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).filter((f) => {
28889
29207
  try {
28890
- return fs65.statSync(path71.join(projectDir, f)).mtimeMs >= monthStartMs;
29208
+ return fs66.statSync(path72.join(projectDir, f)).mtimeMs >= monthStartMs;
28891
29209
  } catch {
28892
29210
  return false;
28893
29211
  }
@@ -28898,7 +29216,7 @@ var HistoryService = class _HistoryService {
28898
29216
  for (const file of files) {
28899
29217
  let raw;
28900
29218
  try {
28901
- raw = fs65.readFileSync(path71.join(projectDir, file), "utf8");
29219
+ raw = fs66.readFileSync(path72.join(projectDir, file), "utf8");
28902
29220
  } catch {
28903
29221
  continue;
28904
29222
  }
@@ -28977,7 +29295,7 @@ var HistoryService = class _HistoryService {
28977
29295
  if (this.runtime.resolveHistoryFile) {
28978
29296
  return this.runtime.resolveHistoryFile(this.cwd, sessionId);
28979
29297
  }
28980
- return path71.join(this.projectDir, `${sessionId}.jsonl`);
29298
+ return path72.join(this.projectDir, `${sessionId}.jsonl`);
28981
29299
  }
28982
29300
  /**
28983
29301
  * Parse a conversation's messages from disk, agent-aware. Claude uses the
@@ -28989,6 +29307,10 @@ var HistoryService = class _HistoryService {
28989
29307
  * convention as parseJsonl.
28990
29308
  */
28991
29309
  readConversation(sessionId) {
29310
+ const agentId = this.runtime.id;
29311
+ return scrubSquadContext(this.readConversationRaw(sessionId)).map((m) => ({ ...m, agentId }));
29312
+ }
29313
+ readConversationRaw(sessionId) {
28992
29314
  if (this.runtime.resolveHistoryFile) {
28993
29315
  const filePath = this.runtime.resolveHistoryFile(this.cwd, sessionId);
28994
29316
  if (!filePath) return [];
@@ -29011,7 +29333,7 @@ var HistoryService = class _HistoryService {
29011
29333
  };
29012
29334
  });
29013
29335
  }
29014
- return parseJsonl(path71.join(this.projectDir, `${sessionId}.jsonl`));
29336
+ return parseJsonl(path72.join(this.projectDir, `${sessionId}.jsonl`));
29015
29337
  }
29016
29338
  async loadConversation(sessionId) {
29017
29339
  const messages = this.readConversation(sessionId);
@@ -29079,7 +29401,7 @@ var HistoryService = class _HistoryService {
29079
29401
  if (!filePath) return false;
29080
29402
  let mtimeMs;
29081
29403
  try {
29082
- mtimeMs = fs65.statSync(filePath).mtimeMs;
29404
+ mtimeMs = fs66.statSync(filePath).mtimeMs;
29083
29405
  } catch {
29084
29406
  return false;
29085
29407
  }
@@ -29154,10 +29476,10 @@ var HistoryService = class _HistoryService {
29154
29476
 
29155
29477
  // src/agents/acp/client.ts
29156
29478
  var import_node_child_process29 = require("child_process");
29157
- var fs66 = __toESM(require("fs/promises"));
29479
+ var fs67 = __toESM(require("fs/promises"));
29158
29480
  var fsSync = __toESM(require("fs"));
29159
- var os57 = __toESM(require("os"));
29160
- var path73 = __toESM(require("path"));
29481
+ var os58 = __toESM(require("os"));
29482
+ var path74 = __toESM(require("path"));
29161
29483
  var import_node_stream = require("stream");
29162
29484
 
29163
29485
  // ../../node_modules/@agentclientprotocol/sdk/dist/schema/index.js
@@ -33183,8 +33505,8 @@ function createIdleTimeout(idleMs, makeError, activeIdleMs = idleMs) {
33183
33505
  }
33184
33506
 
33185
33507
  // src/agents/acp/internal-paths.ts
33186
- var path72 = __toESM(require("path"));
33187
- var os56 = __toESM(require("os"));
33508
+ var path73 = __toESM(require("path"));
33509
+ var os57 = __toESM(require("os"));
33188
33510
  var INTERNAL_TOKENS = [".codeam", "house-claude"];
33189
33511
  var SELF_HOSTED_WORKSPACE_RE = /\.codeam[/\\]self-hosted/gi;
33190
33512
  function textReferencesInternal(text) {
@@ -33192,13 +33514,13 @@ function textReferencesInternal(text) {
33192
33514
  const scrubbed = text.replace(SELF_HOSTED_WORKSPACE_RE, " ");
33193
33515
  return INTERNAL_TOKENS.some((t2) => scrubbed.includes(t2));
33194
33516
  }
33195
- function pathIsInternal(p2, homeDir2 = os56.homedir()) {
33517
+ function pathIsInternal(p2, homeDir2 = os57.homedir()) {
33196
33518
  if (!p2) return false;
33197
- const abs = path72.resolve(p2);
33198
- const home = path72.resolve(homeDir2);
33199
- const within = (root) => abs === root || abs.startsWith(root + path72.sep);
33200
- if (within(path72.join(home, ".codeam", "self-hosted"))) return false;
33201
- return within(path72.join(home, ".codeam")) || within(path72.join(home, ".beads")) || abs === path72.join(home, ".codeam-host.log") || abs.includes(`${path72.sep}house-claude${path72.sep}`) || abs.endsWith(`${path72.sep}house-claude`);
33519
+ const abs = path73.resolve(p2);
33520
+ const home = path73.resolve(homeDir2);
33521
+ const within = (root) => abs === root || abs.startsWith(root + path73.sep);
33522
+ if (within(path73.join(home, ".codeam", "self-hosted"))) return false;
33523
+ return within(path73.join(home, ".codeam")) || within(path73.join(home, ".beads")) || abs === path73.join(home, ".codeam-host.log") || abs.includes(`${path73.sep}house-claude${path73.sep}`) || abs.endsWith(`${path73.sep}house-claude`);
33202
33524
  }
33203
33525
  function toolCallReferencesInternal(call) {
33204
33526
  if (textReferencesInternal(call.title)) return true;
@@ -34157,7 +34479,7 @@ var AcpClient = class {
34157
34479
  throw new RequestError(-32002, GUARDRAIL_SECRET_READ_BLOCK_REASON, { uri: params.path });
34158
34480
  }
34159
34481
  try {
34160
- const content = await fs66.readFile(params.path, "utf8");
34482
+ const content = await fs67.readFile(params.path, "utf8");
34161
34483
  return applyLineRange(content, params.line ?? null, params.limit ?? null);
34162
34484
  } catch (err) {
34163
34485
  const code = err.code;
@@ -34180,7 +34502,7 @@ var AcpClient = class {
34180
34502
  throw new RequestError(-32002, INTERNAL_BLOCK_REASON, { uri: params.path });
34181
34503
  }
34182
34504
  try {
34183
- await fs66.writeFile(params.path, params.content, "utf8");
34505
+ await fs67.writeFile(params.path, params.content, "utf8");
34184
34506
  return {};
34185
34507
  } catch (err) {
34186
34508
  const code = err.code;
@@ -34240,29 +34562,29 @@ function applyLineRange(content, line, limit) {
34240
34562
  return { content: lines.slice(start2, end).join("\n") };
34241
34563
  }
34242
34564
  function knownAgentBinaryDirs() {
34243
- const home = os57.homedir();
34565
+ const home = os58.homedir();
34244
34566
  const out2 = [];
34245
34567
  out2.push("/tmp/codeam-node20/bin");
34246
34568
  for (const root of [
34247
34569
  "/usr/local/share/nvm/versions/node",
34248
- path73.join(home, ".nvm/versions/node")
34570
+ path74.join(home, ".nvm/versions/node")
34249
34571
  ]) {
34250
34572
  try {
34251
34573
  for (const child of fsSync.readdirSync(root)) {
34252
- out2.push(path73.join(root, child, "bin"));
34574
+ out2.push(path74.join(root, child, "bin"));
34253
34575
  }
34254
34576
  } catch {
34255
34577
  }
34256
34578
  }
34257
- out2.push(path73.join(home, ".volta/bin"));
34579
+ out2.push(path74.join(home, ".volta/bin"));
34258
34580
  out2.push("/usr/local/bin");
34259
34581
  out2.push("/usr/bin");
34260
- out2.push(path73.join(home, ".local/bin"));
34261
- out2.push(path73.join(home, "bin"));
34582
+ out2.push(path74.join(home, ".local/bin"));
34583
+ out2.push(path74.join(home, "bin"));
34262
34584
  if (process.platform === "win32") {
34263
34585
  const { LOCALAPPDATA, APPDATA } = process.env;
34264
- if (LOCALAPPDATA) out2.push(path73.join(LOCALAPPDATA, "cursor-agent"));
34265
- if (APPDATA) out2.push(path73.join(APPDATA, "npm"));
34586
+ if (LOCALAPPDATA) out2.push(path74.join(LOCALAPPDATA, "cursor-agent"));
34587
+ if (APPDATA) out2.push(path74.join(APPDATA, "npm"));
34266
34588
  }
34267
34589
  return out2.filter((p2) => {
34268
34590
  try {
@@ -34274,7 +34596,7 @@ function knownAgentBinaryDirs() {
34274
34596
  }
34275
34597
  function expandPathForAgentBinaries(existingPath) {
34276
34598
  const existing = new Set(
34277
- existingPath.split(path73.delimiter).filter((p2) => p2.length > 0)
34599
+ existingPath.split(path74.delimiter).filter((p2) => p2.length > 0)
34278
34600
  );
34279
34601
  const additions = [];
34280
34602
  for (const dir of knownAgentBinaryDirs()) {
@@ -34284,7 +34606,7 @@ function expandPathForAgentBinaries(existingPath) {
34284
34606
  }
34285
34607
  }
34286
34608
  if (additions.length === 0) return existingPath;
34287
- return [...additions, existingPath].filter((p2) => p2.length > 0).join(path73.delimiter);
34609
+ return [...additions, existingPath].filter((p2) => p2.length > 0).join(path74.delimiter);
34288
34610
  }
34289
34611
 
34290
34612
  // src/agents/acp/headroom-budget-proxy.ts
@@ -34467,121 +34789,6 @@ async function performAgentSwitch(deps, rawAgentId, fastPath = {}) {
34467
34789
  return { ok: true, agentId };
34468
34790
  }
34469
34791
 
34470
- // src/agents/acp/squad-roster.ts
34471
- var fs67 = __toESM(require("fs"));
34472
- var os58 = __toESM(require("os"));
34473
- var path74 = __toESM(require("path"));
34474
- var PROMPT_MAX = 500;
34475
- var REPLY_SUMMARY_MAX = 1e3;
34476
- var PREAMBLE_MAX = 2e3;
34477
- var BRIEFING_DEFAULT_MAX = 8e3;
34478
- var GENERIC_SPECIALTY = "general implementation tasks";
34479
- function clip(s, max) {
34480
- return s.length > max ? s.slice(0, max) : s;
34481
- }
34482
- function defaultMember() {
34483
- return { acpSessionId: null, provisioned: false, binaryVerified: false, lastTurnIndex: 0 };
34484
- }
34485
- function journalPathFor(homeDir2, sessionId) {
34486
- return path74.join(homeDir2, ".codeam", `squad-journal-${sessionId}.json`);
34487
- }
34488
- function loadJournal(journalPath) {
34489
- try {
34490
- const raw = JSON.parse(fs67.readFileSync(journalPath, "utf-8"));
34491
- return Array.isArray(raw.turns) ? raw.turns : [];
34492
- } catch {
34493
- return [];
34494
- }
34495
- }
34496
- var SquadState = class {
34497
- /** Set by the caller after fetchSquadRoster; null until then. */
34498
- roster = null;
34499
- journalPath;
34500
- turns;
34501
- members = /* @__PURE__ */ new Map();
34502
- constructor(opts) {
34503
- this.journalPath = journalPathFor(opts.homeDir ?? os58.homedir(), opts.sessionId);
34504
- this.turns = loadJournal(this.journalPath);
34505
- }
34506
- /** Returns (creating on first access) the mutable per-agent state. */
34507
- member(agentId) {
34508
- let m = this.members.get(agentId);
34509
- if (!m) {
34510
- m = defaultMember();
34511
- this.members.set(agentId, m);
34512
- }
34513
- return m;
34514
- }
34515
- /** Appends a journal entry and persists (fire-and-forget durability). */
34516
- recordTurn(entry) {
34517
- const turn = {
34518
- turn: this.turns.length + 1,
34519
- agentId: entry.agentId,
34520
- prompt: clip(entry.prompt, PROMPT_MAX),
34521
- replySummary: clip(entry.replySummary, REPLY_SUMMARY_MAX),
34522
- filesTouched: entry.filesTouched
34523
- };
34524
- this.turns.push(turn);
34525
- this.persist();
34526
- }
34527
- turnCount() {
34528
- return this.turns.length;
34529
- }
34530
- entriesSince(turnIndex) {
34531
- return this.turns.filter((t2) => t2.turn > turnIndex);
34532
- }
34533
- persist() {
34534
- try {
34535
- fs67.mkdirSync(path74.dirname(this.journalPath), { recursive: true, mode: 448 });
34536
- fs67.writeFileSync(this.journalPath, JSON.stringify({ turns: this.turns }), { mode: 384 });
34537
- } catch {
34538
- }
34539
- }
34540
- };
34541
- function specialtyFor(agentId) {
34542
- return SQUAD_SPECIALTIES[agentId] ?? GENERIC_SPECIALTY;
34543
- }
34544
- function buildTeamPreamble(roster, currentAgent, opts) {
34545
- const others = roster.agents.filter((a) => a.agentId !== currentAgent);
34546
- if (others.length === 0) return null;
34547
- const lines = [
34548
- "[Team context] You are the active agent in a CodeAgent Mobile session where the user",
34549
- "has a squad of agents and can pass work between them. Your available teammates:",
34550
- ...others.map((a) => `- ${a.displayName} \u2014 best at: ${specialtyFor(a.agentId)}`)
34551
- ];
34552
- if (opts.handoffInstructions) {
34553
- lines.push(
34554
- "If a task clearly fits a teammate better than you, you MAY propose a handoff by ending",
34555
- `your reply with a fenced code block tagged ${HANDOFF_FENCE_TAG} containing ONE JSON object:`,
34556
- '{"to":"<teammate id>","reason":"<one sentence>","prompt":"<the prompt they should run>"}',
34557
- "Propose at most one handoff per reply, only when genuinely better, and never announce",
34558
- "the block in prose \u2014 the app renders it as a card the user can accept."
34559
- );
34560
- }
34561
- return clip(lines.join("\n"), PREAMBLE_MAX);
34562
- }
34563
- function renderJournalEntry(e) {
34564
- const filesClause = e.filesTouched.length > 0 ? ` (files: ${e.filesTouched.join(", ")})` : "";
34565
- return `- turn ${e.turn} (${e.agentId}): ${e.prompt} \u2192 ${e.replySummary}${filesClause}`;
34566
- }
34567
- function buildDeltaBriefing(entries, maxChars = BRIEFING_DEFAULT_MAX) {
34568
- if (entries.length === 0) return null;
34569
- const header = "[Team update] While you were away, other agents worked on this session:";
34570
- const footer = "Continue from the CURRENT state of the working tree.";
34571
- const envelope = header.length + 1 + footer.length + 1;
34572
- const sorted = [...entries].sort((a, b) => a.turn - b.turn);
34573
- const lines = [];
34574
- let bodyLen = 0;
34575
- for (let i = sorted.length - 1; i >= 0; i--) {
34576
- const line = renderJournalEntry(sorted[i]);
34577
- const addedLen = line.length + (lines.length > 0 ? 1 : 0);
34578
- if (lines.length > 0 && envelope + bodyLen + addedLen > maxChars) break;
34579
- lines.unshift(line);
34580
- bodyLen += addedLen;
34581
- }
34582
- return [header, lines.join("\n"), footer].join("\n");
34583
- }
34584
-
34585
34792
  // src/services/streaming/transport.ts
34586
34793
  var http7 = __toESM(require("http"));
34587
34794
  var https8 = __toESM(require("https"));
@@ -34881,6 +35088,13 @@ var AcpPublisher = class {
34881
35088
  * `mode: 'replace'` so a re-send overrides any stale persisted
34882
35089
  * version — important for the ACP path because each turn we ship
34883
35090
  * the cumulative messages, not a delta.
35091
+ *
35092
+ * Each message may carry its OWN `agentId` — the agent that PRODUCED that
35093
+ * turn. The top-level `agentId` only keys the backend's per-agent
35094
+ * conversation bucket, so without the per-message field a multi-agent
35095
+ * session's reloaded history collapsed to whichever agent happened to be
35096
+ * active (the v1 limitation, codeagent-egai). Additive: older backends
35097
+ * ignore it, older CLIs simply omit it.
34884
35098
  */
34885
35099
  async pushConversation(args2) {
34886
35100
  const url2 = `${this.apiBase}/api/sessions/conversation`;
@@ -35101,8 +35315,19 @@ function stripHandoffFences(text) {
35101
35315
  }
35102
35316
  return restore(stripFences(masked));
35103
35317
  }
35104
- function handoffFenceStart(text) {
35105
- return text.indexOf(FENCE_OPEN);
35318
+ function handoffFenceStartMasked(text) {
35319
+ const { masked } = maskOuterFences(text);
35320
+ if (masked.indexOf(FENCE_OPEN) === -1) return -1;
35321
+ const spans = [];
35322
+ for (const m of text.matchAll(OUTER_FENCE_RE)) {
35323
+ const start2 = m.index ?? 0;
35324
+ spans.push({ start: start2, end: start2 + m[0].length });
35325
+ }
35326
+ const insideSpan = (idx) => spans.some((s) => idx >= s.start && idx < s.end);
35327
+ for (let i = text.indexOf(FENCE_OPEN); i !== -1; i = text.indexOf(FENCE_OPEN, i + 1)) {
35328
+ if (!insideSpan(i)) return i;
35329
+ }
35330
+ return -1;
35106
35331
  }
35107
35332
 
35108
35333
  // src/agents/acp/onboarding.ts
@@ -35882,11 +36107,34 @@ var TurnFileAggregator = class {
35882
36107
  */
35883
36108
  baselineByKey = /* @__PURE__ */ new Map();
35884
36109
  baselineCaptured = false;
36110
+ /**
36111
+ * The file paths (relative to their repo root) that the MOST RECENT
36112
+ * `flushTurn()` call found novel — i.e. what it just enqueued (or would
36113
+ * have enqueued had the batch not exactly matched the baseline). Updated
36114
+ * unconditionally on every flush, including a no-op one (empties back
36115
+ * out), so `peekTurnPaths()` never returns paths from more than one
36116
+ * flush cycle ago. Not touched by the baseline-capture flush (it returns
36117
+ * before `novel` is computed), which is correct — pre-pair files aren't
36118
+ * "this session's" changes.
36119
+ */
36120
+ lastTurnPaths = [];
35885
36121
  /** Stop the outbox scheduler. Idempotent. */
35886
36122
  stop() {
35887
36123
  this.stopped = true;
35888
36124
  this.outbox.stop();
35889
36125
  }
36126
+ /**
36127
+ * Non-destructive read of the paths the MOST RECENT `flushTurn()` call
36128
+ * found novel — does NOT trigger a scan or clear anything itself. The
36129
+ * squad journal (`recordSquadTurn` in `command-handlers.ts`) `await`s
36130
+ * `flushTurn()` for the turn it's about to record BEFORE calling this, so
36131
+ * the read reflects THAT turn's paths, not a stale one left over from
36132
+ * whatever flushed previously. Empty on a session's very first turn
36133
+ * (still capturing the pre-pair baseline) or a chat-only turn.
36134
+ */
36135
+ peekTurnPaths() {
36136
+ return [...this.lastTurnPaths];
36137
+ }
35890
36138
  /**
35891
36139
  * Run the discovery + git collection + POST pipeline for one
35892
36140
  * turn. Errors are swallowed (logged) so an agent never blocks on a
@@ -35927,6 +36175,7 @@ var TurnFileAggregator = class {
35927
36175
  if (!base) return true;
35928
36176
  return base.linesAdded !== f.linesAdded || base.linesRemoved !== f.linesRemoved || base.fileStatus !== f.fileStatus;
35929
36177
  });
36178
+ this.lastTurnPaths = novel.map((f) => f.filePath);
35930
36179
  if (novel.length === 0) {
35931
36180
  log.trace(
35932
36181
  "turnFiles",
@@ -35946,10 +36195,7 @@ var TurnFileAggregator = class {
35946
36195
  await this.outbox.enqueue(entry);
35947
36196
  }
35948
36197
  } catch (err) {
35949
- log.warn(
35950
- "turnFiles",
35951
- `flushTurn failed: ${err.message ?? String(err)}`
35952
- );
36198
+ log.warn("turnFiles", `flushTurn failed: ${err.message ?? String(err)}`);
35953
36199
  }
35954
36200
  }
35955
36201
  /**
@@ -36945,6 +37191,56 @@ async function detectRepoStack(cwd, runtime) {
36945
37191
  }
36946
37192
  }
36947
37193
 
37194
+ // src/agents/acp/coderabbit-mention.ts
37195
+ var CODERABBIT_AGENT_ID = "coderabbit";
37196
+ var CODERABBIT_NOT_LINKED_MESSAGE = "Link CodeRabbit in Profile \u203A Your Squad first.";
37197
+ var CODERABBIT_NO_CUSTOM_INSTRUCTIONS_NOTICE = "CodeRabbit reviews your current changes \u2014 custom instructions aren't supported yet.";
37198
+ function hasCustomInstructions(prompt) {
37199
+ return prompt.replace(/@coderabbit\b/gi, "").trim().length > 0;
37200
+ }
37201
+ function composeReviewOutput(markdown, custom) {
37202
+ const body = markdown.trim().length > 0 ? markdown.trim() : "CodeRabbit found no issues to report.";
37203
+ return custom ? `${CODERABBIT_NO_CUSTOM_INSTRUCTIONS_NOTICE}
37204
+
37205
+ ${body}` : body;
37206
+ }
37207
+ function defaultRunReview(input) {
37208
+ return new CoderabbitRuntimeStrategy(createOsStrategy()).runOneShot(input);
37209
+ }
37210
+ async function runCoderabbitMentionReview(deps = {}) {
37211
+ const configure = deps.configure ?? configureCoderabbit;
37212
+ const runReview = deps.runReview ?? defaultRunReview;
37213
+ try {
37214
+ const status2 = await configure({ action: "status" });
37215
+ if (!status2.loggedIn) {
37216
+ const cred = deps.fetchCredential ? await deps.fetchCredential() : null;
37217
+ if (!cred) return { ok: false, error: CODERABBIT_NOT_LINKED_MESSAGE };
37218
+ const provisioned = await configure({
37219
+ action: "provision",
37220
+ provisionCredential: cred
37221
+ });
37222
+ if (!provisioned.loggedIn) {
37223
+ return { ok: false, error: provisioned.error ?? CODERABBIT_NOT_LINKED_MESSAGE };
37224
+ }
37225
+ }
37226
+ const result = await configure({ action: "review" }, { runReview });
37227
+ if (result.error) return { ok: false, error: result.error };
37228
+ return { ok: true, markdown: result.review?.markdown ?? "" };
37229
+ } catch (err) {
37230
+ return {
37231
+ ok: false,
37232
+ error: err instanceof Error ? err.message : "CodeRabbit review failed"
37233
+ };
37234
+ }
37235
+ }
37236
+ function logMentionOutcome(result) {
37237
+ if (result.ok) {
37238
+ log.info("acpRunner", `squad: coderabbit review completed (${result.markdown.length} chars)`);
37239
+ } else {
37240
+ log.warn("acpRunner", `squad: coderabbit review failed: ${result.error}`);
37241
+ }
37242
+ }
37243
+
36948
37244
  // src/agents/acp/command-handlers.ts
36949
37245
  var import_node_child_process31 = require("child_process");
36950
37246
 
@@ -37143,39 +37439,72 @@ async function routeSquadTask(ctx, target) {
37143
37439
  }
37144
37440
  return { ok: true };
37145
37441
  }
37146
- function prefixSquadContext(ctx, blocks) {
37442
+ function collectSquadContext(ctx) {
37443
+ const pieces = [];
37444
+ const { squad, opts } = ctx;
37445
+ if (squad) {
37446
+ const member = squad.member(opts.agent);
37447
+ const roster = squad.roster;
37448
+ if (roster && member.lastTurnIndex === 0) {
37449
+ const preamble = buildTeamPreamble(roster, opts.agent, {
37450
+ handoffInstructions: roster.handoffsEnabled === true
37451
+ });
37452
+ if (preamble) pieces.push(preamble);
37453
+ }
37454
+ if (squad.turnCount() > member.lastTurnIndex) {
37455
+ const otherEntries = squad.entriesSince(member.lastTurnIndex).filter((e) => e.agentId !== opts.agent);
37456
+ const briefing = buildDeltaBriefing(otherEntries);
37457
+ if (briefing) pieces.push(briefing);
37458
+ }
37459
+ }
37147
37460
  if (ctx.pendingHandoff?.current) {
37148
- blocks.unshift({ type: "text", text: ctx.pendingHandoff.current });
37461
+ pieces.push(ctx.pendingHandoff.current);
37149
37462
  ctx.pendingHandoff.current = null;
37150
37463
  }
37151
- const { squad, opts } = ctx;
37152
- if (!squad) return;
37153
- const member = squad.member(opts.agent);
37154
- if (squad.turnCount() > member.lastTurnIndex) {
37155
- const otherEntries = squad.entriesSince(member.lastTurnIndex).filter((e) => e.agentId !== opts.agent);
37156
- const briefing = buildDeltaBriefing(otherEntries);
37157
- if (briefing) blocks.unshift({ type: "text", text: briefing });
37464
+ return pieces;
37465
+ }
37466
+ function squadContextMode(ctx) {
37467
+ return ctx.squad?.member(ctx.opts.agent).contextTextFallback === true ? "text" : "resource";
37468
+ }
37469
+ function applySquadContext(blocks, pieces, mode) {
37470
+ if (pieces.length === 0) return;
37471
+ if (mode === "resource") {
37472
+ blocks.unshift(buildSquadContextBlock(pieces.join("\n\n")));
37473
+ return;
37158
37474
  }
37159
- const roster = squad.roster;
37160
- if (roster && member.lastTurnIndex === 0) {
37161
- const preamble = buildTeamPreamble(roster, opts.agent, {
37162
- handoffInstructions: roster.handoffsEnabled === true
37163
- });
37164
- if (preamble) blocks.unshift({ type: "text", text: preamble });
37475
+ for (let i = pieces.length - 1; i >= 0; i--) blocks.unshift({ type: "text", text: pieces[i] });
37476
+ }
37477
+ async function promptWithContextFallback(ctx, client3, blocks, pieces) {
37478
+ try {
37479
+ return await client3.prompt(blocks);
37480
+ } catch (err) {
37481
+ if (pieces.length === 0 || blocks.length === 0 || !isSquadContextBlock(blocks[0]) || !looksLikeUnsupportedPromptShape(err)) {
37482
+ throw err;
37483
+ }
37484
+ log.warn(
37485
+ "acpRunner",
37486
+ `squad: ${ctx.opts.agent} rejected the native squad-context resource block (${describeError(err)}) \u2014 retrying once with legacy text blocks`
37487
+ );
37488
+ if (ctx.squad) ctx.squad.member(ctx.opts.agent).contextTextFallback = true;
37489
+ blocks.shift();
37490
+ applySquadContext(blocks, pieces, "text");
37491
+ return await client3.prompt(blocks);
37165
37492
  }
37166
37493
  }
37167
37494
  function recordSquadTurn(ctx, prompt, replySummary) {
37168
- const { squad, opts } = ctx;
37495
+ const { squad, opts, turnFiles } = ctx;
37169
37496
  if (!squad) return;
37170
37497
  squad.recordTurn({
37171
37498
  agentId: opts.agent,
37172
37499
  prompt,
37173
37500
  replySummary,
37174
37501
  // TurnFileAggregator owns per-turn file changesets end-to-end (git diff →
37175
- // outbox POST) and exposes no path list, and its flush is fire-and-forget,
37176
- // so there is nothing accurate to attribute synchronously here. The
37177
- // briefing simply omits the files clause rather than guessing.
37178
- filesTouched: []
37502
+ // outbox POST). The caller (`startTaskH`) AWAITS `turnFiles.flushTurn()`
37503
+ // for THIS turn before calling recordSquadTurn precisely so
37504
+ // `peekTurnPaths()` reflects THIS turn's novel files, not a stale read
37505
+ // of whatever the aggregator's PREVIOUS flush happened to find. Capped
37506
+ // so a pathological turn (mass refactor) doesn't bloat the journal.
37507
+ filesTouched: turnFiles.peekTurnPaths().slice(0, 20)
37179
37508
  });
37180
37509
  squad.member(opts.agent).lastTurnIndex = squad.turnCount();
37181
37510
  }
@@ -37190,45 +37519,112 @@ function resolvePendingProposal(ctx, requestedAgentId) {
37190
37519
  if (!slot || !pending) return;
37191
37520
  slot.current = null;
37192
37521
  const accepted = requestedAgentId === pending.toAgentId;
37522
+ if (accepted) ctx.squad?.countAccepted();
37193
37523
  log.info(
37194
37524
  "acpRunner",
37195
37525
  `squad: handoff ${pending.proposalId} ${accepted ? "accepted" : "declined"}`
37196
37526
  );
37197
- void ctx.postSquadEvent?.("handoff_resolved", {
37198
- proposalId: pending.proposalId,
37199
- accepted
37200
- });
37527
+ const resolution = { proposalId: pending.proposalId, accepted };
37528
+ void ctx.postSquadEvent?.("handoff_resolved", { ...resolution });
37529
+ }
37530
+ function proposalIdFor(commandId, hop) {
37531
+ const id = hop <= 1 ? `hp-${commandId}` : `hp-${commandId}-h${hop}`;
37532
+ return id.length > 128 ? id.slice(0, 128) : id;
37201
37533
  }
37202
- function emitHandoffProposal(ctx, proposal) {
37534
+ function emitHandoffProposal(ctx, proposal, hop) {
37203
37535
  const { squad, pendingProposal, postSquadEvent, opts, cmd } = ctx;
37204
- if (!proposal || !pendingProposal || !postSquadEvent) return;
37205
- if (squad?.roster?.handoffsEnabled !== true) return;
37536
+ if (!proposal || !pendingProposal || !postSquadEvent) return null;
37537
+ if (squad?.roster?.handoffsEnabled !== true) return null;
37206
37538
  if (pendingProposal.current) {
37207
37539
  log.info("acpRunner", "squad: dropping handoff proposal \u2014 one is already pending");
37208
- return;
37540
+ return null;
37209
37541
  }
37542
+ const auto = squad.auto.enabled && squad.hopsRemaining() > 0;
37543
+ if (auto) squad.consumeHop();
37210
37544
  const record2 = {
37211
- proposalId: `hp-${cmd.id}`,
37545
+ proposalId: proposalIdFor(cmd.id, hop),
37212
37546
  fromAgentId: opts.agent,
37213
37547
  toAgentId: proposal.to,
37214
37548
  reason: proposal.reason,
37215
- prompt: proposal.prompt
37549
+ prompt: proposal.prompt,
37550
+ ...auto ? { auto: true, hopsRemaining: squad.hopsRemaining() } : {}
37216
37551
  };
37217
- pendingProposal.current = record2;
37218
- log.info("acpRunner", `squad: handoff proposed ${opts.agent} \u2192 ${record2.toAgentId}`);
37552
+ squad.countProposal({ auto });
37553
+ log.info(
37554
+ "acpRunner",
37555
+ `squad: handoff proposed ${opts.agent} \u2192 ${record2.toAgentId}${auto ? " (auto)" : ""}`
37556
+ );
37219
37557
  void postSquadEvent("handoff_proposed", { ...record2 });
37558
+ if (!auto) {
37559
+ pendingProposal.current = record2;
37560
+ return null;
37561
+ }
37562
+ return record2;
37563
+ }
37564
+ function resolveAutoHandoff(ctx, record2) {
37565
+ ctx.squad?.countAccepted();
37566
+ const resolution = {
37567
+ proposalId: record2.proposalId,
37568
+ accepted: true,
37569
+ auto: true
37570
+ };
37571
+ void ctx.postSquadEvent?.("handoff_resolved", { ...resolution });
37572
+ }
37573
+ async function runCoderabbitMention(ctx, promptText) {
37574
+ const { cmd, relay, streaming, opts, turnFiles } = ctx;
37575
+ const userText = promptText.length > 0 ? promptText : `@${CODERABBIT_AGENT_ID}`;
37576
+ await streaming.beginTurn();
37577
+ ctx.history.appendUserPrompt(userText);
37578
+ log.info("acpRunner", `squad: coderabbit one-shot review id=${cmd.id.slice(0, 8)}`);
37579
+ const review = await runCoderabbitMentionReview({
37580
+ fetchCredential: () => fetchProvisionCredential({
37581
+ agentId: CODERABBIT_AGENT_ID,
37582
+ sessionId: opts.sessionId,
37583
+ pluginId: opts.pluginId,
37584
+ pluginAuthToken: opts.pluginAuthToken
37585
+ })
37586
+ });
37587
+ logMentionOutcome(review);
37588
+ if (!review.ok) {
37589
+ await streaming.closeWithBubble(review.error);
37590
+ ctx.history.appendAgentReply(review.error, CODERABBIT_AGENT_ID);
37591
+ void ctx.history.flush();
37592
+ await relay.sendResult(cmd.id, "failed", { error: review.error });
37593
+ return;
37594
+ }
37595
+ const output = composeReviewOutput(review.markdown, hasCustomInstructions(promptText));
37596
+ await streaming.closeWithBubble(output);
37597
+ ctx.history.appendAgentReply(output, CODERABBIT_AGENT_ID);
37598
+ void ctx.history.flush();
37599
+ await turnFiles.flushTurn().catch((err) => {
37600
+ log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37601
+ });
37602
+ recordCoderabbitTurn(ctx, userText, output);
37603
+ await relay.sendResult(cmd.id, "completed", { agentId: CODERABBIT_AGENT_ID });
37604
+ }
37605
+ function recordCoderabbitTurn(ctx, prompt, replySummary) {
37606
+ ctx.squad?.recordTurn({
37607
+ agentId: CODERABBIT_AGENT_ID,
37608
+ prompt,
37609
+ replySummary,
37610
+ filesTouched: []
37611
+ });
37220
37612
  }
37221
37613
  async function startTaskH(ctx) {
37222
37614
  const { cmd, relay, streaming, opts, turnFiles, publisher, recentStderr, budgetReachedFlag } = ctx;
37223
37615
  const payload = cmd.payload;
37616
+ const requestedAgentId = typeof payload?.agentId === "string" ? payload.agentId.trim() : "";
37617
+ resolvePendingProposal(ctx, requestedAgentId);
37618
+ if (requestedAgentId === CODERABBIT_AGENT_ID) {
37619
+ await runCoderabbitMention(ctx, (payload?.prompt ?? "").trim());
37620
+ return;
37621
+ }
37224
37622
  const blocks = buildAcpPromptBlocks(payload ?? {});
37225
37623
  if (blocks.length === 0) {
37226
37624
  log.warn("acpRunner", "start_task with empty prompt + no attachments; ignoring");
37227
37625
  await relay.sendResult(cmd.id, "failed", { error: "empty prompt" });
37228
37626
  return;
37229
37627
  }
37230
- const requestedAgentId = typeof payload?.agentId === "string" ? payload.agentId.trim() : "";
37231
- resolvePendingProposal(ctx, requestedAgentId);
37232
37628
  if (requestedAgentId.length > 0 && requestedAgentId !== opts.agent) {
37233
37629
  const routed = await routeSquadTask(ctx, requestedAgentId);
37234
37630
  if (!routed.ok) {
@@ -37237,7 +37633,6 @@ async function startTaskH(ctx) {
37237
37633
  return;
37238
37634
  }
37239
37635
  }
37240
- const { client: client3, history, budgetRecovery } = ctx;
37241
37636
  const promptText = blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
37242
37637
  const imageCount = blocks.filter((b) => b.type === "image").length;
37243
37638
  log.info(
@@ -37249,133 +37644,214 @@ async function startTaskH(ctx) {
37249
37644
  showInfo(echoLine);
37250
37645
  }
37251
37646
  await streaming.beginTurn();
37252
- history.appendUserPrompt(promptText);
37647
+ ctx.history.appendUserPrompt(promptText);
37253
37648
  maybePrefaceAgentStandard(blocks, opts.agent, opts.sessionId);
37254
- prefixSquadContext(ctx, blocks);
37255
- let turnClosed = false;
37256
- try {
37257
- const reply = await client3.prompt(blocks);
37258
- const finalText = streaming.getCurrentText();
37259
- if (agentHooks(opts.agent)?.classifyCompletedReply?.(finalText) === "upgrade_required") {
37260
- await streaming.closeWithBubble(CURSOR_UPGRADE_MESSAGE);
37261
- turnClosed = true;
37262
- history.appendAgentReply(CURSOR_UPGRADE_MESSAGE);
37263
- void history.flush();
37264
- log.info("acpRunner", `start_task \u2190 cursor-plan-upgrade-required id=${cmd.id.slice(0, 8)}`);
37265
- await relay.sendResult(cmd.id, "failed", { error: "cursor plan upgrade required" });
37266
- } else if (replyIsHouseAgentLimit(finalText)) {
37267
- const houseBubble = houseAgentLimitMessage(finalText);
37268
- await streaming.closeWithBubble(houseBubble);
37269
- turnClosed = true;
37270
- history.appendAgentReply(houseBubble);
37271
- void history.flush();
37272
- turnFiles.flushTurn().catch((err) => {
37273
- log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37274
- });
37275
- log.info("acpRunner", `start_task \u2190 house-agent-limit id=${cmd.id.slice(0, 8)}`);
37276
- await relay.sendResult(cmd.id, "failed", {
37277
- error: "house agent usage ceiling / temporarily unavailable"
37278
- });
37279
- } else if (replyIsAuthFailure(finalText)) {
37280
- await streaming.closeWithBubble(AUTH_FAILURE_MESSAGE);
37281
- turnClosed = true;
37282
- history.appendAgentReply(AUTH_FAILURE_MESSAGE);
37283
- void history.flush();
37284
- turnFiles.flushTurn().catch((err) => {
37285
- log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37286
- });
37287
- void reportCredentialInvalid(opts);
37288
- log.info("acpRunner", `start_task \u2190 auth-failure-in-reply id=${cmd.id.slice(0, 8)}`);
37289
- await relay.sendResult(cmd.id, "failed", { error: "agent reply reported auth failure" });
37290
- } else if (shouldOfferOneMRecovery({ detail: "", recentStderr: recentStderr.join("\n"), finalText })) {
37291
- await streaming.closeWithBubble(ONE_M_CREDITS_MESSAGE);
37292
- turnClosed = true;
37293
- history.appendAgentReply(ONE_M_CREDITS_MESSAGE);
37294
- void history.flush();
37295
- turnFiles.flushTurn().catch((err) => {
37296
- log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37297
- });
37298
- void reportCredentialInvalid(opts);
37299
- log.info("acpRunner", `start_task \u2190 1m-credits-reconnect id=${cmd.id.slice(0, 8)}`);
37300
- await relay.sendResult(cmd.id, "failed", {
37301
- error: "agent reply reported 1M-context usage-credits gate"
37302
- });
37303
- } else {
37304
- await streaming.closeTurnWithInteractiveDetection();
37305
- turnClosed = true;
37306
- const { cleanText, proposal } = extractHandoffProposal(
37307
- finalText,
37308
- opts.agent,
37309
- handoffTargets(ctx)
37310
- );
37311
- const replyLine = formatAgentReplyLine(cleanText);
37312
- if (replyLine.length > 0) {
37313
- showInfo(replyLine);
37314
- }
37315
- history.appendAgentReply(cleanText);
37316
- void history.flush();
37317
- recordSquadTurn(ctx, promptText, cleanText);
37318
- emitHandoffProposal(ctx, proposal);
37319
- void publisher.publishOutput({
37320
- type: "input_suggestion",
37321
- content: ACP_QUICK_REPLIES,
37322
- done: true
37323
- });
37324
- turnFiles.flushTurn().catch((err) => {
37325
- log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37326
- });
37327
- log.info("acpRunner", `start_task \u2190 done stopReason=${reply.stopReason ?? "?"} id=${cmd.id.slice(0, 8)}`);
37328
- await relay.sendResult(cmd.id, "completed", { stopReason: reply.stopReason });
37329
- }
37330
- } catch (err) {
37331
- if (turnClosed) {
37332
- log.warn(
37333
- "acpRunner",
37334
- `post-close ack failed (turn already delivered) id=${cmd.id.slice(0, 8)}: ${describeError(err)}`
37335
- );
37336
- return;
37337
- }
37338
- const hadText = streaming.hasVisibleProgress();
37339
- const detail = describeError(err);
37340
- log.warn("acpRunner", `prompt failed: ${detail}`);
37341
- await cancelStuckTurn(client3);
37342
- if (looksLikeBudgetExceeded(`${detail}
37343
- ${recentStderr.join("\n")}`)) {
37344
- await streaming.closeAll();
37345
- if (!budgetReachedFlag.get()) {
37346
- budgetReachedFlag.set(true);
37347
- void postBudgetReached({
37348
- sessionId: opts.sessionId,
37349
- pluginId: opts.pluginId,
37350
- pluginAuthToken: opts.pluginAuthToken,
37351
- agent: opts.agent,
37352
- period: extractBudgetPeriod(`${detail}
37353
- ${recentStderr.join("\n")}`)
37649
+ const squadContext = collectSquadContext(ctx);
37650
+ applySquadContext(blocks, squadContext, squadContextMode(ctx));
37651
+ ctx.squad?.resetHops();
37652
+ let turnBlocks = blocks;
37653
+ let turnPieces = squadContext;
37654
+ let turnPrompt = promptText;
37655
+ let hop = 1;
37656
+ for (; ; ) {
37657
+ const { client: client3, history, budgetRecovery } = ctx;
37658
+ let turnClosed = false;
37659
+ try {
37660
+ const reply = await promptWithContextFallback(ctx, client3, turnBlocks, turnPieces);
37661
+ const finalText = streaming.getCurrentText();
37662
+ if (agentHooks(opts.agent)?.classifyCompletedReply?.(finalText) === "upgrade_required") {
37663
+ await streaming.closeWithBubble(CURSOR_UPGRADE_MESSAGE);
37664
+ turnClosed = true;
37665
+ history.appendAgentReply(CURSOR_UPGRADE_MESSAGE);
37666
+ void history.flush();
37667
+ log.info("acpRunner", `start_task \u2190 cursor-plan-upgrade-required id=${cmd.id.slice(0, 8)}`);
37668
+ await relay.sendResult(cmd.id, "failed", { error: "cursor plan upgrade required" });
37669
+ return;
37670
+ } else if (replyIsHouseAgentLimit(finalText)) {
37671
+ const houseBubble = houseAgentLimitMessage(finalText);
37672
+ await streaming.closeWithBubble(houseBubble);
37673
+ turnClosed = true;
37674
+ history.appendAgentReply(houseBubble);
37675
+ void history.flush();
37676
+ turnFiles.flushTurn().catch((err) => {
37677
+ log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37678
+ });
37679
+ log.info("acpRunner", `start_task \u2190 house-agent-limit id=${cmd.id.slice(0, 8)}`);
37680
+ await relay.sendResult(cmd.id, "failed", {
37681
+ error: "house agent usage ceiling / temporarily unavailable"
37682
+ });
37683
+ return;
37684
+ } else if (replyIsAuthFailure(finalText)) {
37685
+ await streaming.closeWithBubble(AUTH_FAILURE_MESSAGE);
37686
+ turnClosed = true;
37687
+ history.appendAgentReply(AUTH_FAILURE_MESSAGE);
37688
+ void history.flush();
37689
+ turnFiles.flushTurn().catch((err) => {
37690
+ log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37691
+ });
37692
+ void reportCredentialInvalid(opts);
37693
+ log.info("acpRunner", `start_task \u2190 auth-failure-in-reply id=${cmd.id.slice(0, 8)}`);
37694
+ await relay.sendResult(cmd.id, "failed", { error: "agent reply reported auth failure" });
37695
+ return;
37696
+ } else if (shouldOfferOneMRecovery({ detail: "", recentStderr: recentStderr.join("\n"), finalText })) {
37697
+ await streaming.closeWithBubble(ONE_M_CREDITS_MESSAGE);
37698
+ turnClosed = true;
37699
+ history.appendAgentReply(ONE_M_CREDITS_MESSAGE);
37700
+ void history.flush();
37701
+ turnFiles.flushTurn().catch((err) => {
37702
+ log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37703
+ });
37704
+ void reportCredentialInvalid(opts);
37705
+ log.info("acpRunner", `start_task \u2190 1m-credits-reconnect id=${cmd.id.slice(0, 8)}`);
37706
+ await relay.sendResult(cmd.id, "failed", {
37707
+ error: "agent reply reported 1M-context usage-credits gate"
37708
+ });
37709
+ return;
37710
+ } else {
37711
+ await streaming.closeTurnWithInteractiveDetection();
37712
+ turnClosed = true;
37713
+ const { cleanText, proposal } = extractHandoffProposal(
37714
+ finalText,
37715
+ opts.agent,
37716
+ handoffTargets(ctx)
37717
+ );
37718
+ const replyLine = formatAgentReplyLine(cleanText);
37719
+ if (replyLine.length > 0) {
37720
+ showInfo(replyLine);
37721
+ }
37722
+ history.appendAgentReply(cleanText);
37723
+ void history.flush();
37724
+ const flush = turnFiles.flushTurn().catch((err) => {
37725
+ log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
37726
+ });
37727
+ if (ctx.squad) await flush;
37728
+ recordSquadTurn(ctx, turnPrompt, cleanText);
37729
+ const autoHop = emitHandoffProposal(ctx, proposal, hop);
37730
+ if (autoHop) {
37731
+ const routed = await routeSquadTask(ctx, autoHop.toAgentId);
37732
+ if (routed.ok) {
37733
+ resolveAutoHandoff(ctx, autoHop);
37734
+ hop += 1;
37735
+ turnPrompt = autoHop.prompt;
37736
+ turnPieces = collectSquadContext(ctx);
37737
+ turnBlocks = [{ type: "text", text: turnPrompt }];
37738
+ maybePrefaceAgentStandard(turnBlocks, opts.agent, opts.sessionId);
37739
+ applySquadContext(turnBlocks, turnPieces, squadContextMode(ctx));
37740
+ await streaming.beginTurn();
37741
+ ctx.history.appendUserPrompt(turnPrompt);
37742
+ continue;
37743
+ }
37744
+ log.warn(
37745
+ "acpRunner",
37746
+ `squad: auto-handoff to ${autoHop.toAgentId} failed: ${routed.error}`
37747
+ );
37748
+ }
37749
+ void publisher.publishOutput({
37750
+ type: "input_suggestion",
37751
+ content: ACP_QUICK_REPLIES,
37752
+ done: true
37354
37753
  });
37754
+ log.info(
37755
+ "acpRunner",
37756
+ `start_task \u2190 done stopReason=${reply.stopReason ?? "?"} id=${cmd.id.slice(0, 8)}`
37757
+ );
37758
+ await relay.sendResult(cmd.id, "completed", { stopReason: reply.stopReason });
37759
+ return;
37355
37760
  }
37356
- await budgetRecovery.offer(cmd.id, blocks, `${detail}
37761
+ } catch (err) {
37762
+ if (turnClosed) {
37763
+ log.warn(
37764
+ "acpRunner",
37765
+ `post-close ack failed (turn already delivered) id=${cmd.id.slice(0, 8)}: ${describeError(err)}`
37766
+ );
37767
+ return;
37768
+ }
37769
+ const hadText = streaming.hasVisibleProgress();
37770
+ const detail = describeError(err);
37771
+ log.warn("acpRunner", `prompt failed: ${detail}`);
37772
+ await cancelStuckTurn(client3);
37773
+ if (looksLikeBudgetExceeded(`${detail}
37774
+ ${recentStderr.join("\n")}`)) {
37775
+ await streaming.closeAll();
37776
+ if (!budgetReachedFlag.get()) {
37777
+ budgetReachedFlag.set(true);
37778
+ void postBudgetReached({
37779
+ sessionId: opts.sessionId,
37780
+ pluginId: opts.pluginId,
37781
+ pluginAuthToken: opts.pluginAuthToken,
37782
+ agent: opts.agent,
37783
+ period: extractBudgetPeriod(`${detail}
37784
+ ${recentStderr.join("\n")}`)
37785
+ });
37786
+ }
37787
+ await budgetRecovery.offer(cmd.id, turnBlocks, `${detail}
37357
37788
  ${recentStderr.join("\n")}`);
37789
+ return;
37790
+ }
37791
+ const bubble = failureBubble({
37792
+ detail,
37793
+ recentStderr: recentStderr.join("\n"),
37794
+ hadText,
37795
+ agent: opts.agent
37796
+ });
37797
+ if (bubble) {
37798
+ await streaming.closeWithBubble(bubble);
37799
+ history.appendAgentReply(bubble);
37800
+ void history.flush();
37801
+ } else {
37802
+ await streaming.closeAll();
37803
+ }
37804
+ if (bubble === AUTH_FAILURE_MESSAGE || bubble === ONE_M_CREDITS_MESSAGE) {
37805
+ void reportCredentialInvalid(opts);
37806
+ }
37807
+ await relay.sendResult(cmd.id, "failed", { error: detail });
37358
37808
  return;
37359
37809
  }
37360
- const bubble = failureBubble({
37361
- detail,
37362
- recentStderr: recentStderr.join("\n"),
37363
- hadText,
37364
- agent: opts.agent
37810
+ }
37811
+ }
37812
+ async function squadConfigureH(ctx) {
37813
+ const { cmd, relay, opts, squad } = ctx;
37814
+ if (!squad) {
37815
+ await relay.sendResult(cmd.id, "failed", {
37816
+ error: "Agent Squad is not available on this session."
37365
37817
  });
37366
- if (bubble) {
37367
- await streaming.closeWithBubble(bubble);
37368
- history.appendAgentReply(bubble);
37369
- void history.flush();
37370
- } else {
37371
- await streaming.closeAll();
37372
- }
37373
- if (bubble === AUTH_FAILURE_MESSAGE || bubble === ONE_M_CREDITS_MESSAGE) {
37374
- void reportCredentialInvalid(opts);
37375
- }
37376
- await relay.sendResult(cmd.id, "failed", { error: detail });
37818
+ return;
37377
37819
  }
37378
- return;
37820
+ const payload = cmd.payload;
37821
+ if (payload?.action === "set") {
37822
+ const applied = squad.setAuto({
37823
+ enabled: payload.autoHandoffs === true,
37824
+ hopBudget: clampHopBudget(payload.hopBudget)
37825
+ });
37826
+ setSquadAuto(opts.pluginId, applied);
37827
+ log.info(
37828
+ "acpRunner",
37829
+ `squad: auto handoffs ${applied.enabled ? "ON" : "OFF"} budget=${applied.hopBudget}`
37830
+ );
37831
+ const result = { ...applied, hopsRemaining: squad.hopsRemaining() };
37832
+ await relay.sendResult(cmd.id, "completed", { ...result });
37833
+ return;
37834
+ }
37835
+ if (payload?.action === "status") {
37836
+ const result = {
37837
+ ...squad.auto,
37838
+ hopsRemaining: squad.hopsRemaining()
37839
+ };
37840
+ await relay.sendResult(cmd.id, "completed", { ...result });
37841
+ return;
37842
+ }
37843
+ await relay.sendResult(cmd.id, "failed", { error: "squad_configure: unknown action" });
37844
+ }
37845
+ async function squadStatsH(ctx) {
37846
+ const { cmd, relay, squad } = ctx;
37847
+ if (!squad) {
37848
+ await relay.sendResult(cmd.id, "failed", {
37849
+ error: "Agent Squad is not available on this session."
37850
+ });
37851
+ return;
37852
+ }
37853
+ const stats = squad.stats();
37854
+ await relay.sendResult(cmd.id, "completed", { ...stats });
37379
37855
  }
37380
37856
  async function groupMentionTaskH(ctx) {
37381
37857
  const { cmd, client: client3, relay, streaming, opts, history } = ctx;
@@ -37388,6 +37864,7 @@ async function groupMentionTaskH(ctx) {
37388
37864
  });
37389
37865
  return;
37390
37866
  }
37867
+ ctx.squad?.resetHops();
37391
37868
  await streaming.beginTurn();
37392
37869
  history.appendUserPrompt(promptText);
37393
37870
  let response = "";
@@ -37782,7 +38259,10 @@ async function integrationsSyncH(ctx) {
37782
38259
  attached: manifest.integrations.map((e) => e.id)
37783
38260
  });
37784
38261
  } catch (err) {
37785
- log.warn("acpRunner", `integrations_sync failed (tools apply next restart): ${describeError(err)}`);
38262
+ log.warn(
38263
+ "acpRunner",
38264
+ `integrations_sync failed (tools apply next restart): ${describeError(err)}`
38265
+ );
37786
38266
  await relay.sendResult(cmd.id, "completed", { synced: false, error: describeError(err) });
37787
38267
  }
37788
38268
  }
@@ -37852,7 +38332,9 @@ var ACP_COMMAND_HANDLERS = {
37852
38332
  skills_configure: skillsConfigureH2,
37853
38333
  pack_start: packStartH,
37854
38334
  pack_action: packActionH,
37855
- pack_status: packStatusH
38335
+ pack_status: packStatusH,
38336
+ [SQUAD_CONFIGURE_COMMAND]: squadConfigureH,
38337
+ [SQUAD_STATS_COMMAND]: squadStatsH
37856
38338
  };
37857
38339
  async function dispatchAcpCommand(ctx) {
37858
38340
  const handler = ACP_COMMAND_HANDLERS[ctx.cmd.type];
@@ -38117,7 +38599,7 @@ var StreamingState = class {
38117
38599
  let fenceCut = -1;
38118
38600
  if (delta.kind === "text") {
38119
38601
  this.recomputeText();
38120
- fenceCut = handoffFenceStart(this.text);
38602
+ fenceCut = handoffFenceStartMasked(this.text);
38121
38603
  const visibleText = fenceCut === -1 ? this.text : this.text.slice(0, fenceCut).trimEnd();
38122
38604
  void this.publisher.publishOutput({ type: "text", content: visibleText, done: false });
38123
38605
  }
@@ -38165,11 +38647,12 @@ var StreamingState = class {
38165
38647
  * {@link getCurrentText} keeps returning the RAW text so the turn-close
38166
38648
  * extraction can parse the proposal out of it.
38167
38649
  *
38168
- * Masked-aware (`stripHandoffFences`), unlike the live-stream `append()`
38169
- * cut (`handoffFenceStart`, unmasked): a TERMINAL frame is the one that
38650
+ * Masked-aware (`stripHandoffFences`), same as the live-stream `append()`
38651
+ * cut (`handoffFenceStartMasked`): a TERMINAL frame is the one that
38170
38652
  * PERSISTS, so cutting on a fence quoted as an example inside a
38171
- * 4+-backtick block here would permanently truncate the bubble. The live
38172
- * cut stays unmasked/cheap a mid-stream example is never terminal.
38653
+ * 4+-backtick block here would permanently truncate the bubble and an
38654
+ * unmasked live cut would truncate the LIVE view for the rest of the turn
38655
+ * the moment the quoted example's fence-open marker streams in.
38173
38656
  */
38174
38657
  visible(text) {
38175
38658
  return stripHandoffFences(text);
@@ -38345,16 +38828,24 @@ var AcpHistory = class {
38345
38828
  id: (0, import_node_crypto11.randomUUID)(),
38346
38829
  role: "user",
38347
38830
  text,
38348
- timestamp: Date.now()
38831
+ timestamp: Date.now(),
38832
+ agentId: this.opts.agent
38349
38833
  });
38350
38834
  }
38351
- appendAgentReply(text) {
38835
+ /**
38836
+ * `agentId` overrides the producing agent for a turn this session's RESIDENT
38837
+ * agent did not author — today only the `@coderabbit` one-shot review, which
38838
+ * runs the batch reviewer inside the session without ever swapping. Defaults
38839
+ * to the resident agent.
38840
+ */
38841
+ appendAgentReply(text, agentId = this.opts.agent) {
38352
38842
  if (text.length === 0) return;
38353
38843
  this.messages.push({
38354
38844
  id: (0, import_node_crypto11.randomUUID)(),
38355
38845
  role: "agent",
38356
38846
  text,
38357
- timestamp: Date.now()
38847
+ timestamp: Date.now(),
38848
+ agentId
38358
38849
  });
38359
38850
  }
38360
38851
  /**
@@ -38694,6 +39185,8 @@ async function runAcpSession(opts) {
38694
39185
  pluginAuthToken: opts.pluginAuthToken,
38695
39186
  agentId: opts.agent
38696
39187
  });
39188
+ void turnFiles.flushTurn().catch(() => {
39189
+ });
38697
39190
  const REPO_DIRTY_FLUSH_DEBOUNCE_MS = 2e3;
38698
39191
  let repoDirtyTimer = null;
38699
39192
  const fileWatcher = new FileWatcherService({
@@ -38739,9 +39232,12 @@ async function runAcpSession(opts) {
38739
39232
  publisher,
38740
39233
  recentStderr,
38741
39234
  budgetRecovery,
38742
- { get: () => _budgetReachedPosted, set: (v) => {
38743
- _budgetReachedPosted = v;
38744
- } },
39235
+ {
39236
+ get: () => _budgetReachedPosted,
39237
+ set: (v) => {
39238
+ _budgetReachedPosted = v;
39239
+ }
39240
+ },
38745
39241
  // resume_session re-points the runner's active conversation: the
38746
39242
  // relay callback reads `acpSessionId` per command, so every FUTURE
38747
39243
  // get_conversation / upload / one-shot serves the RESUMED id — not
@@ -38765,7 +39261,10 @@ async function runAcpSession(opts) {
38765
39261
  );
38766
39262
  const HANDOFF_MAX_CHARS = 16e3;
38767
39263
  const pendingHandoff = { current: null };
38768
- const squad = new SquadState({ sessionId: opts.sessionId });
39264
+ const squad = new SquadState({
39265
+ sessionId: opts.sessionId,
39266
+ auto: getSquadAuto(opts.pluginId)
39267
+ });
38769
39268
  const refreshSquadRoster = () => {
38770
39269
  void fetchSquadRoster({
38771
39270
  sessionId: opts.sessionId,
@@ -43765,7 +44264,7 @@ function checkChokidar() {
43765
44264
  }
43766
44265
  async function doctor(args2 = []) {
43767
44266
  const json = args2.includes("--json");
43768
- const cliVersion = true ? "2.64.0" : "0.0.0-dev";
44267
+ const cliVersion = true ? "2.65.0" : "0.0.0-dev";
43769
44268
  const apiBase2 = resolveApiBaseUrl();
43770
44269
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
43771
44270
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -44156,7 +44655,7 @@ async function mcpRun(args2) {
44156
44655
  // src/commands/version.ts
44157
44656
  var import_picocolors15 = __toESM(require("picocolors"));
44158
44657
  function version2() {
44159
- const v = true ? "2.64.0" : "unknown";
44658
+ const v = true ? "2.65.0" : "unknown";
44160
44659
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
44161
44660
  }
44162
44661