squadrant 0.13.0 → 0.13.2

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/index.js CHANGED
@@ -622,8 +622,41 @@ function resolveWorktreeBase(repoRoot, fallback = "develop") {
622
622
  return fallback;
623
623
  }
624
624
  function addWorktree(spec) {
625
- const wt = worktreePath(spec.repoRoot, spec.worktreeDir, spec.project, spec.name);
626
- execFileSync2("git", ["-C", spec.repoRoot, "worktree", "add", wt, "-b", crewBranch(spec.name), spec.base], { stdio: "pipe" });
625
+ const originalBranch = crewBranch(spec.name);
626
+ let targetName = spec.name;
627
+ let targetBranch = originalBranch;
628
+ let branchExists = false;
629
+ try {
630
+ execFileSync2("git", ["-C", spec.repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${originalBranch}`], { stdio: "pipe" });
631
+ branchExists = true;
632
+ } catch {
633
+ }
634
+ if (branchExists) {
635
+ const log = execFileSync2("git", ["-C", spec.repoRoot, "log", "--oneline", `${spec.base}..${originalBranch}`], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
636
+ if (!log) {
637
+ execFileSync2("git", ["-C", spec.repoRoot, "branch", "-D", originalBranch], { stdio: "pipe" });
638
+ } else {
639
+ let suffix = 2;
640
+ while (true) {
641
+ const candidate = `${spec.name}-${suffix}`;
642
+ const candidateBranch = crewBranch(candidate);
643
+ let candidateExists = false;
644
+ try {
645
+ execFileSync2("git", ["-C", spec.repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${candidateBranch}`], { stdio: "pipe" });
646
+ candidateExists = true;
647
+ } catch {
648
+ }
649
+ if (!candidateExists) {
650
+ targetName = candidate;
651
+ targetBranch = candidateBranch;
652
+ break;
653
+ }
654
+ suffix++;
655
+ }
656
+ }
657
+ }
658
+ const wt = worktreePath(spec.repoRoot, spec.worktreeDir, spec.project, targetName);
659
+ execFileSync2("git", ["-C", spec.repoRoot, "worktree", "add", wt, "-b", targetBranch, spec.base], { stdio: "pipe" });
627
660
  return wt;
628
661
  }
629
662
  function removeWorktree(repoRoot, wtPath) {
@@ -2454,8 +2487,9 @@ async function runCodexInteractiveSpawn(o) {
2454
2487
  title
2455
2488
  });
2456
2489
  await o.runtime.sendToPane(pane, `squadrant crew attach ${rec.id}`);
2457
- if (o.task && o.task !== "(interactive)") {
2458
- void o.sendCodexFirstTurn(rec.id, o.task).catch((e) => {
2490
+ const firstTurnText = o.firstTurn ?? o.task;
2491
+ if (firstTurnText && firstTurnText !== "(interactive)") {
2492
+ void o.sendCodexFirstTurn(rec.id, firstTurnText).catch((e) => {
2459
2493
  process.stderr.write(`(first-turn delivery failed: ${e.message})
2460
2494
  `);
2461
2495
  });
@@ -2487,6 +2521,13 @@ async function runCrewSpawn(input, config, deps) {
2487
2521
  name,
2488
2522
  base: resolveWorktreeBase(proj.path)
2489
2523
  }) : proj.path;
2524
+ let firstTurnTask = input.task;
2525
+ if (input.taskFile && input.taskFile !== "-" && !input.shared) {
2526
+ const absTaskFile = path10.resolve(input.taskFile);
2527
+ const basename = path10.basename(absTaskFile);
2528
+ fs12.copyFileSync(absTaskFile, path10.join(spawnCwd, basename));
2529
+ firstTurnTask = `Read ./${basename} to get your task brief, then execute it.`;
2530
+ }
2490
2531
  const route = !input.agentExplicit && !input.model ? resolveCrewRoute(input.task, config) : null;
2491
2532
  if (route) {
2492
2533
  deps.onRouted?.(route);
@@ -2502,6 +2543,7 @@ async function runCrewSpawn(input, config, deps) {
2502
2543
  return runCodexInteractiveSpawn({
2503
2544
  project: input.project,
2504
2545
  task: input.task,
2546
+ firstTurn: firstTurnTask !== input.task ? firstTurnTask : void 0,
2505
2547
  cwd: spawnCwd,
2506
2548
  runtime: deps.runtime,
2507
2549
  workspaceId: captain.id,
@@ -2545,9 +2587,15 @@ async function runCrewSpawn(input, config, deps) {
2545
2587
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
2546
2588
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${cliCommand2}`);
2547
2589
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
2548
- await deps.sendFirstTurn(pane2, `${input.task}
2590
+ const claudeResult = await deps.sendFirstTurn(pane2, `${firstTurnTask}
2549
2591
 
2550
2592
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
2593
+ if (!claudeResult.delivered) {
2594
+ process.stderr.write(`\u26A0\uFE0F First turn not delivered for crew '${name}' \u2014 use 'squadrant crew send ${input.project} ${name}' to re-send the task.
2595
+ `);
2596
+ } else {
2597
+ await deps.emitEvent?.(input.project, { type: "task.first-turn.confirmed", id: rec.id });
2598
+ }
2551
2599
  return { ...pane2, title: title2 };
2552
2600
  }
2553
2601
  if (agentName === "opencode") {
@@ -2587,7 +2635,7 @@ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
2587
2635
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
2588
2636
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} OPENCODE_CONFIG=${opencodeConfigPath} ${cliCommand2}`);
2589
2637
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
2590
- await deps.sendFirstTurn(pane2, `${input.task}
2638
+ const opencodeResult = await deps.sendFirstTurn(pane2, `${firstTurnTask}
2591
2639
 
2592
2640
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
2593
2641
  // #235: confirm-on-delivery — sendFirstTurnWhenReady polls until "Ask
@@ -2595,6 +2643,12 @@ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
2595
2643
  // without duplicating the task. See crew-pane.ts SPLASH_MAX_CHECKS/EVERY_N.
2596
2644
  splashMarker: "Ask anything\u2026"
2597
2645
  });
2646
+ if (!opencodeResult.delivered) {
2647
+ process.stderr.write(`\u26A0\uFE0F First turn not delivered for crew '${name}' \u2014 use 'squadrant crew send ${input.project} ${name}' to re-send the task.
2648
+ `);
2649
+ } else {
2650
+ await deps.emitEvent?.(input.project, { type: "task.first-turn.confirmed", id: rec.id });
2651
+ }
2598
2652
  return { ...pane2, title: title2 };
2599
2653
  }
2600
2654
  const cliCommand = agent.buildCommand({
@@ -2611,7 +2665,11 @@ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
2611
2665
  await deps.runtime.sendToPane(pane, cliCommand);
2612
2666
  if (interactive) {
2613
2667
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane) ?? "";
2614
- await deps.sendFirstTurn(pane, input.task, preLaunchScreen);
2668
+ const genericResult = await deps.sendFirstTurn(pane, firstTurnTask, preLaunchScreen);
2669
+ if (!genericResult.delivered) {
2670
+ process.stderr.write(`\u26A0\uFE0F First turn not delivered for crew '${name}' \u2014 use 'squadrant crew send ${input.project} ${name}' to re-send the task.
2671
+ `);
2672
+ }
2615
2673
  }
2616
2674
  return { ...pane, title };
2617
2675
  }
@@ -2632,8 +2690,12 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps) {
2632
2690
  }
2633
2691
  } catch {
2634
2692
  }
2635
- const deliver = deps.sendToPane ?? ((pane, msg) => runtime.sendToPane(pane, msg));
2636
- await deliver(crew, message);
2693
+ const deliver = deps.sendToPane ?? ((pane, msg) => runtime.sendToPane(pane, msg).then(() => ({ delivered: true })));
2694
+ const { delivered } = await deliver(crew, message);
2695
+ if (!delivered) {
2696
+ process.stderr.write(`\u26A0\uFE0F Message not delivered to crew '${name}' \u2014 use 'squadrant crew send ${project} ${name}' to re-send.
2697
+ `);
2698
+ }
2637
2699
  }
2638
2700
  async function runCrewRead(project, name, runtime, workspaceId) {
2639
2701
  const crew = await findCrewPane(runtime, workspaceId, project, name);
@@ -3911,13 +3973,18 @@ var init_native_hook_source = __esm({
3911
3973
  import net from "net";
3912
3974
  async function settleInputBox(runtime, pane) {
3913
3975
  let prev = await runtime.readPaneScreen(pane) ?? "";
3976
+ let sawContent = parseDraftFromScreen(prev) !== "" && parseDraftFromScreen(prev) !== null;
3914
3977
  for (let i = 0; i < SETTLE_MAX_POLLS; i++) {
3915
3978
  await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
3916
3979
  const cur = await runtime.readPaneScreen(pane) ?? "";
3980
+ const draft = parseDraftFromScreen(cur);
3981
+ if (draft !== "" && draft !== null)
3982
+ sawContent = true;
3917
3983
  if (cur === prev)
3918
- return;
3984
+ return sawContent;
3919
3985
  prev = cur;
3920
3986
  }
3987
+ return sawContent;
3921
3988
  }
3922
3989
  function getFreePort() {
3923
3990
  return new Promise((resolve3, reject) => {
@@ -3955,19 +4022,29 @@ async function resolveCaptainWorkspace(project) {
3955
4022
  async function confirmedSendToPane(runtime, pane, message) {
3956
4023
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
3957
4024
  await runtime.pasteToPane(pane, message);
3958
- await settleInputBox(runtime, pane);
4025
+ let sawDraft = await settleInputBox(runtime, pane);
3959
4026
  await runtime.sendKeyToPane(pane, "Enter");
4027
+ let repasted = false;
3960
4028
  for (let attempt = 0; attempt < SUBMIT_RETRY_LIMIT; attempt++) {
3961
4029
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
3962
4030
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
3963
4031
  const draft = parseDraftFromScreen(afterScreen);
3964
- if (draft === "")
3965
- return;
4032
+ if (draft !== "" && draft !== null)
4033
+ sawDraft = true;
4034
+ if (draft === "" && sawDraft)
4035
+ return { delivered: true };
3966
4036
  if (draft === null && afterScreen !== preSendScreen)
3967
- return;
3968
- await settleInputBox(runtime, pane);
4037
+ return { delivered: true };
4038
+ const settled = await settleInputBox(runtime, pane);
4039
+ if (settled)
4040
+ sawDraft = true;
4041
+ if (!sawDraft && !repasted) {
4042
+ repasted = true;
4043
+ await runtime.pasteToPane(pane, message);
4044
+ }
3969
4045
  await runtime.sendKeyToPane(pane, "Enter");
3970
4046
  }
4047
+ return { delivered: false };
3971
4048
  }
3972
4049
  async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acceptanceConfig) {
3973
4050
  await new Promise((r) => setTimeout(r, SEND_FIRST_TURN_FLOOR_MS));
@@ -3976,7 +4053,8 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
3976
4053
  let stable = false;
3977
4054
  for (let i = 0; i < maxPolls && !stable; i++) {
3978
4055
  const screen = await runtime.readPaneScreen(pane) ?? "";
3979
- if (screen.length > 0 && screen === previousScreen && screen !== preLaunchScreen) {
4056
+ const hasInputBox = !!acceptanceConfig?.splashMarker || parseDraftFromScreen(screen) !== null;
4057
+ if (screen.length > 0 && screen === previousScreen && screen !== preLaunchScreen && hasInputBox) {
3980
4058
  stable = true;
3981
4059
  } else {
3982
4060
  previousScreen = screen;
@@ -3990,29 +4068,45 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
3990
4068
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
3991
4069
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
3992
4070
  if (isTurnAccepted(preSendScreen, afterScreen, acceptanceConfig)) {
3993
- return;
4071
+ return { delivered: true };
3994
4072
  }
3995
4073
  if ((check2 + 1) % SPLASH_RESEND_EVERY_N === 0 && check2 < SPLASH_MAX_CHECKS - 1) {
3996
4074
  await runtime.sendToPane(pane, task);
3997
4075
  }
3998
4076
  }
3999
- return;
4077
+ return { delivered: false };
4078
+ }
4079
+ if (!stable) {
4080
+ return confirmedSendToPane(runtime, pane, task);
4000
4081
  }
4001
4082
  await runtime.pasteToPane(pane, task);
4002
- await settleInputBox(runtime, pane);
4083
+ let sawDraft = await settleInputBox(runtime, pane);
4003
4084
  await runtime.sendKeyToPane(pane, "Enter");
4004
4085
  const retryLimit = acceptanceConfig?.retryLimit ?? SUBMIT_RETRY_LIMIT;
4086
+ let repasted = false;
4005
4087
  for (let attempt = 0; attempt < retryLimit; attempt++) {
4006
4088
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
4007
4089
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
4008
4090
  const draft = parseDraftFromScreen(afterScreen);
4009
- if (draft === "")
4010
- return;
4091
+ if (draft !== "" && draft !== null)
4092
+ sawDraft = true;
4093
+ if (draft === "" && sawDraft)
4094
+ return { delivered: true };
4011
4095
  if (draft === null && afterScreen !== preSendScreen)
4012
- return;
4013
- await settleInputBox(runtime, pane);
4096
+ return { delivered: true };
4097
+ const settled = await settleInputBox(runtime, pane);
4098
+ if (settled)
4099
+ sawDraft = true;
4100
+ if (!sawDraft && !repasted) {
4101
+ repasted = true;
4102
+ await runtime.pasteToPane(pane, task);
4103
+ }
4014
4104
  await runtime.sendKeyToPane(pane, "Enter");
4015
4105
  }
4106
+ if (!sawDraft) {
4107
+ return confirmedSendToPane(runtime, pane, task);
4108
+ }
4109
+ return { delivered: false };
4016
4110
  }
4017
4111
  var SEND_FIRST_TURN_FLOOR_MS, POLL_INTERVAL_MS, SEND_FIRST_TURN_TIMEOUT_MS, POST_SEND_CHECK_MS, SPLASH_MAX_CHECKS, SPLASH_RESEND_EVERY_N, SETTLE_POLL_MS, SETTLE_MAX_POLLS, SUBMIT_RETRY_LIMIT;
4018
4112
  var init_crew_pane = __esm({
@@ -5806,8 +5900,13 @@ function classifyPaneTail(tail) {
5806
5900
  if (c != null && ERROR_BANNER_RE.some((re) => re.test(c)))
5807
5901
  errLine = c;
5808
5902
  }
5809
- if (errLine)
5903
+ if (errLine) {
5904
+ const isRetrying = cleaned.some((c) => c != null && RETRYING_RE.test(c));
5905
+ const isExhausted = cleaned.some((c) => c != null && EXHAUSTED_RE.test(c));
5906
+ if (isRetrying && !isExhausted)
5907
+ return null;
5810
5908
  return { kind: "error", text: errLine.slice(0, 200) };
5909
+ }
5811
5910
  return null;
5812
5911
  }
5813
5912
  function stripChrome(raw) {
@@ -5824,7 +5923,7 @@ function stripChrome(raw) {
5824
5923
  return null;
5825
5924
  return trimmed;
5826
5925
  }
5827
- var ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
5926
+ var ERROR_BANNER_RE, RETRYING_RE, EXHAUSTED_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
5828
5927
  var init_pane_classifier = __esm({
5829
5928
  "packages/agents/dist/interactive/pane-classifier.js"() {
5830
5929
  init_claude2();
@@ -5837,6 +5936,8 @@ var init_pane_classifier = __esm({
5837
5936
  /\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b/i,
5838
5937
  /\bmaximum\s+retries\b/i
5839
5938
  ];
5939
+ RETRYING_RE = /\bRetrying\b|\battempt\s+\d+\s*\/\s*\d+/i;
5940
+ EXHAUSTED_RE = /\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b|\bmaximum\s+retries\b/i;
5840
5941
  OPTION_RE = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
5841
5942
  PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
5842
5943
  PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
@@ -6979,6 +7080,7 @@ import chalk9 from "chalk";
6979
7080
  // packages/cli/src/commands/crew-control.ts
6980
7081
  init_dist2();
6981
7082
  init_dist2();
7083
+ init_dist();
6982
7084
  init_dist4();
6983
7085
  import { Command as Command8 } from "commander";
6984
7086
  import { createConnection as createConnection3 } from "net";
@@ -7404,7 +7506,16 @@ function addControlPlaneCrewCommands(crew) {
7404
7506
  const r = await squadrantdCall(buildStatusRequest(project, id));
7405
7507
  process.stdout.write(JSON.stringify(r) + "\n");
7406
7508
  });
7407
- crew.command("tasks <project>").description("List control-plane tasks for a project (control-plane analogue of legacy `list`), or purge a task with --purge").option("--json", "Full JSON output (one or more records)").option("--id <taskId>", "Show only tasks matching this id prefix").option("--state <state>", "Filter by task state").option("--state-only <taskId>", "Print just the state string for a single task").option("--purge <taskId>", "Purge a task record from the store (default: only terminal records)").option("--force", "Force-purge a non-terminal record (use with --purge)").action(async (project, opts) => {
7509
+ crew.command("tasks <project>").description("List control-plane tasks for a project (control-plane analogue of legacy `list`), or purge a task with --purge").option("--json", "Full JSON output (one or more records)").option("--id <taskId>", "Show only tasks matching this id prefix").option("--state <state>", "Filter by task state").option("--state-only <taskId>", "Print just the state string for a single task").option("--purge <taskId>", "Purge a task record from the store (default: only terminal records)").option("--force", "Force-purge a non-terminal record (use with --purge)").option("--all-terminal", "Purge all terminal (done/cancelled/failed) records for the project").action(async (project, opts) => {
7510
+ if (opts.allTerminal) {
7511
+ const tasks = await squadrantdCall({ kind: "list", project });
7512
+ const terminal = tasks.filter((t) => TERMINAL_STATES.has(t.state));
7513
+ for (const t of terminal) {
7514
+ await squadrantdCall({ kind: "purge", project, id: t.id, force: false });
7515
+ }
7516
+ console.log(`purged ${terminal.length} terminal record(s)`);
7517
+ return;
7518
+ }
7408
7519
  if (opts.purge) {
7409
7520
  const r = await squadrantdCall({ kind: "purge", project, id: opts.purge, force: opts.force ?? false });
7410
7521
  console.log(`purged ${r.provider}/${r.id} (was ${r.state})`);
@@ -7642,6 +7753,10 @@ async function runCrewSpawn2(input) {
7642
7753
  sendFirstTurn: (pane, firstTurn, preLaunchScreen, opts) => sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen, opts),
7643
7754
  getFreePort,
7644
7755
  sendCodexFirstTurn,
7756
+ // #466: wire delivery confirmation so the daemon stamps firstTurnConfirmedAt.
7757
+ emitEvent: async (p, event) => {
7758
+ await squadrantdCall({ kind: "event", project: p, event });
7759
+ },
7645
7760
  onRouted: (route) => console.log(
7646
7761
  chalk9.dim(
7647
7762
  `routed: tier=${route.tier} \u2192 ${route.agent}${route.model ? `/${route.model}` : ""} (rule: "${route.matchedRule}")`
@@ -7702,7 +7817,10 @@ crewCommand.command("spawn").description(
7702
7817
  // opencode consumes the `approval` flag (→ bash:"ask" per-crew config).
7703
7818
  ...opts.approval ? { approvalPolicy: "untrusted", approval: true } : {},
7704
7819
  ...opts.shared ? { shared: true } : {},
7705
- ...opts.model ? { model: opts.model } : {}
7820
+ ...opts.model ? { model: opts.model } : {},
7821
+ // #458: pass the raw file path (not stdin) so runCrewSpawn can copy it
7822
+ // into the isolated worktree root for relative-path access.
7823
+ ...opts.taskFile && opts.taskFile !== "-" ? { taskFile: opts.taskFile } : {}
7706
7824
  });
7707
7825
  console.log(chalk9.green(`\u2714 Crew '${pane.title}' spawned (${pane.surfaceId})`));
7708
7826
  } catch (err) {