squadrant 0.16.2 → 0.16.3

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
@@ -5880,6 +5880,9 @@ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
5880
5880
  }
5881
5881
  return { ...pane, title };
5882
5882
  }
5883
+ function pickMostRecentTask(tasks) {
5884
+ return tasks.reduce((a, b) => (b.createdAt ?? 0) > (a.createdAt ?? 0) ? b : a);
5885
+ }
5883
5886
  async function runCrewSend(project, name, message, runtime, workspaceId, deps) {
5884
5887
  const crew = await findCrewPane(runtime, workspaceId, project, name);
5885
5888
  if (!crew) {
@@ -5890,8 +5893,8 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps) {
5890
5893
  throw new Error(blockedByModalMessage());
5891
5894
  }
5892
5895
  try {
5893
- const tasks = await deps.listTasks(project);
5894
- const task = tasks.find((t) => t.name === name);
5896
+ const matches = (await deps.listTasks(project)).filter((t) => t.name === name);
5897
+ const task = matches.length > 0 ? pickMostRecentTask(matches) : void 0;
5895
5898
  if (task) {
5896
5899
  if (TERMINAL_STATES.has(task.state)) {
5897
5900
  await deps.emitEvent(project, { type: "task.reopened", id: task.id });
@@ -5929,7 +5932,7 @@ async function runCrewClose(project, name, runtime, workspaceId, deps) {
5929
5932
  matches = (await deps.listTasks(project)).filter((t) => t.name === name);
5930
5933
  }
5931
5934
  if (matches.length > 0) {
5932
- const primary = matches.reduce((a, b) => (b.createdAt ?? 0) > (a.createdAt ?? 0) ? b : a);
5935
+ const primary = pickMostRecentTask(matches);
5933
5936
  taskId = primary.id;
5934
5937
  if (primary.cwd && projRoot && primary.cwd !== projRoot) {
5935
5938
  worktreeCwd = primary.cwd;
@@ -9341,16 +9344,23 @@ function isPermissionNotification(message) {
9341
9344
  const lower = message.toLowerCase();
9342
9345
  return lower.includes("permission") || lower.includes("approve");
9343
9346
  }
9347
+ function installHookEntry(hooks, event, matcher, command) {
9348
+ if (!Array.isArray(hooks[event]))
9349
+ hooks[event] = [];
9350
+ const entries = hooks[event];
9351
+ const already = entries.some((m) => m?.matcher === matcher && Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h?.command === "string" && h.command.includes(command)));
9352
+ if (!already) {
9353
+ entries.push({ matcher, hooks: [{ type: "command", command, timeout: 10 }] });
9354
+ }
9355
+ }
9344
9356
  function mergeClaudeHooks(settings, hookCmd) {
9345
9357
  const next = structuredClone(settings ?? {});
9346
9358
  next.hooks ??= {};
9347
9359
  for (const ev of EVENTS) {
9348
- if (!Array.isArray(next.hooks[ev]))
9349
- next.hooks[ev] = [];
9350
- const already = next.hooks[ev].some((m) => Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command.includes(hookCmd)));
9351
- if (!already) {
9352
- next.hooks[ev].push({ matcher: "", hooks: [{ type: "command", command: `${hookCmd} ${ev}`, timeout: 10 }] });
9353
- }
9360
+ installHookEntry(next.hooks, ev, "", `${hookCmd} ${ev}`);
9361
+ }
9362
+ for (const [ev, matcher] of MATCHED_EVENTS) {
9363
+ installHookEntry(next.hooks, ev, matcher, `${hookCmd} ${ev}`);
9354
9364
  }
9355
9365
  return next;
9356
9366
  }
@@ -9430,8 +9440,32 @@ function resolveLastAssistantText(payload) {
9430
9440
  }
9431
9441
  return null;
9432
9442
  }
9443
+ function formatAskUserQuestionPrompt(toolInput) {
9444
+ const questions = toolInput?.questions;
9445
+ if (!Array.isArray(questions) || questions.length === 0)
9446
+ return null;
9447
+ const parts = [];
9448
+ for (const q of questions) {
9449
+ if (!q || typeof q !== "object")
9450
+ continue;
9451
+ const text = q.question;
9452
+ if (typeof text !== "string" || !text.trim())
9453
+ continue;
9454
+ const options = Array.isArray(q.options) ? q.options : [];
9455
+ const labels = options.map((o) => o && typeof o.label === "string" ? o.label.trim() : null).filter((l) => !!l);
9456
+ parts.push(labels.length > 0 ? `${text.trim()} (options: ${labels.join(", ")})` : text.trim());
9457
+ }
9458
+ return parts.length > 0 ? parts.join(" | ") : null;
9459
+ }
9433
9460
  function mapClaudeHookToEvent(event, payload, taskId) {
9434
9461
  switch (event) {
9462
+ case "PreToolUse": {
9463
+ const toolName = payload?.tool_name;
9464
+ if (toolName !== "AskUserQuestion")
9465
+ return null;
9466
+ const question = formatAskUserQuestionPrompt(payload?.tool_input) ?? "crew opened an AskUserQuestion prompt (options unavailable)";
9467
+ return { type: "task.input.requested", id: taskId, requestId: nextAskUserQuestionRequestId++, question };
9468
+ }
9435
9469
  case "Stop": {
9436
9470
  const text = resolveLastAssistantText(payload);
9437
9471
  const question = text ? detectTrailingQuestion2(text) : null;
@@ -9458,10 +9492,14 @@ function mapClaudeHookToEvent(event, payload, taskId) {
9458
9492
  return null;
9459
9493
  }
9460
9494
  }
9461
- var EVENTS, claudeInteractive;
9495
+ var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, claudeInteractive;
9462
9496
  var init_claude2 = __esm({
9463
9497
  "packages/agents/dist/interactive/claude.js"() {
9464
9498
  EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit"];
9499
+ MATCHED_EVENTS = [
9500
+ ["PreToolUse", "AskUserQuestion"]
9501
+ ];
9502
+ nextAskUserQuestionRequestId = Date.now();
9465
9503
  claudeInteractive = {
9466
9504
  provider: "claude",
9467
9505
  tier: "strong",
@@ -9814,6 +9852,7 @@ __export(dist_exports4, {
9814
9852
  createOpencodeEmitter: () => createOpencodeEmitter,
9815
9853
  deriveTranscriptPath: () => deriveTranscriptPath,
9816
9854
  detectTrailingQuestion: () => detectTrailingQuestion2,
9855
+ formatAskUserQuestionPrompt: () => formatAskUserQuestionPrompt,
9817
9856
  getHeadlessAdapter: () => getHeadlessAdapter,
9818
9857
  getInteractiveAdapter: () => getInteractiveAdapter,
9819
9858
  isPermissionNotification: () => isPermissionNotification,
@@ -11310,6 +11349,22 @@ function defaultWriteResult(id, payload) {
11310
11349
  writeFileSync10(file, payload);
11311
11350
  return file;
11312
11351
  }
11352
+ async function runCrewSignal(signal, o, deps) {
11353
+ const taskId = o.taskId ?? process.env.SQUADRANT_CREW_TASK_ID;
11354
+ const project = o.project ?? process.env.SQUADRANT_CREW_PROJECT;
11355
+ if (!taskId)
11356
+ throw new Error("not running under a crew (SQUADRANT_CREW_TASK_ID unset)");
11357
+ if (!project)
11358
+ throw new Error("not running under a crew (SQUADRANT_CREW_PROJECT unset)");
11359
+ const current = await deps.call(buildStatusRequest(project, taskId));
11360
+ if (current && TERMINAL_STATES.has(current.state)) {
11361
+ throw new Error(
11362
+ `Task ${taskId} is already terminal (state=${current.state}) \u2014 signal '${signal}' would be silently ignored by the daemon. Stop here: your task record was never reopened for this turn. Ask the captain to run 'squadrant crew send' to reopen it before signaling again.`
11363
+ );
11364
+ }
11365
+ const req = buildSignalRequest(signal, { ...o, writeResult: o.writeResult ?? defaultWriteResult });
11366
+ await deps.call(req);
11367
+ }
11313
11368
  function addControlPlaneCrewCommands(crew) {
11314
11369
  crew.command("dispatch <project> <task>").description("Dispatch a crew task via the control-plane daemon").requiredOption("--provider <p>", "claude|opencode|codex (gemini: experimental, headless not supported)").option("--mode <m>", "headless|interactive", "interactive").option("--cwd <dir>", "working dir for the crew (project/worktree); required for codex to edit code").action(async (project, task, opts) => {
11315
11370
  const req = buildDispatchRequest({ project, task, provider: opts.provider, mode: opts.mode, cwd: opts.cwd });
@@ -11391,15 +11446,14 @@ function addControlPlaneCrewCommands(crew) {
11391
11446
  process.exit(2);
11392
11447
  }
11393
11448
  try {
11394
- const req = buildSignalRequest(state, {
11449
+ await runCrewSignal(state, {
11395
11450
  ...opts.message !== void 0 ? { message: opts.message } : {},
11396
11451
  ...opts.question !== void 0 ? { question: opts.question } : {},
11397
11452
  ...opts.error !== void 0 ? { error: opts.error } : {},
11398
11453
  ...opts.taskId !== void 0 ? { taskId: opts.taskId } : {},
11399
11454
  ...opts.project !== void 0 ? { project: opts.project } : {},
11400
11455
  writeResult: defaultWriteResult
11401
- });
11402
- await squadrantdCall(req);
11456
+ }, { call: squadrantdCall });
11403
11457
  process.exit(0);
11404
11458
  } catch (e) {
11405
11459
  process.stderr.write(`${e.message}
@@ -14631,22 +14685,38 @@ var EFFORT_MEANING = {
14631
14685
  balance: "normal routing \u2014 use default crew routing rules unchanged",
14632
14686
  low: "conserve tokens \u2014 bias crew spawns toward opencode/sonnet; reserve opus for work that genuinely needs it"
14633
14687
  };
14634
- function runEffortGet(configPath = DEFAULT_CONFIG_PATH, projectName) {
14688
+ function runEffortGet(configPath = DEFAULT_CONFIG_PATH, projectName, projectConfigRoot) {
14635
14689
  const config = loadConfig(configPath);
14636
- const effort = resolveEffort(config, projectName);
14690
+ if (projectName && !(projectName in config.projects)) {
14691
+ const known = Object.keys(config.projects).sort().join(", ") || "(no projects registered)";
14692
+ throw new Error(`Unknown project '${projectName}'. Known projects: ${known}`);
14693
+ }
14694
+ const effort = resolveEffort(config, projectName, projectConfigRoot);
14637
14695
  const description = `${effort}: ${EFFORT_MEANING[effort]}`;
14638
14696
  return { effort, description };
14639
14697
  }
14640
- function runEffortSet(value, configPath = DEFAULT_CONFIG_PATH) {
14698
+ function runEffortSet(value, configPath = DEFAULT_CONFIG_PATH, projectName, projectConfigRoot) {
14641
14699
  if (!VALID_EFFORTS.includes(value)) {
14642
14700
  throw new Error(
14643
14701
  `Invalid effort '${value}'. Valid values: ${VALID_EFFORTS.join(" | ")}`
14644
14702
  );
14645
14703
  }
14704
+ if (projectName) {
14705
+ const config2 = loadConfig(configPath);
14706
+ if (!(projectName in config2.projects)) {
14707
+ const known = Object.keys(config2.projects).sort().join(", ") || "(no projects registered)";
14708
+ throw new Error(`Unknown project '${projectName}'. Known projects: ${known}`);
14709
+ }
14710
+ saveProjectOverride(projectName, { effort: value }, projectConfigRoot);
14711
+ return;
14712
+ }
14646
14713
  const config = loadConfig(configPath);
14647
14714
  config.defaults.effort = value;
14648
14715
  saveConfig(config, configPath);
14649
14716
  }
14717
+ function effortScopeLabel(projectName) {
14718
+ return projectName ? `project: ${projectName}` : "global";
14719
+ }
14650
14720
  function canonical(p) {
14651
14721
  try {
14652
14722
  return fs28.realpathSync(p);
@@ -14654,11 +14724,16 @@ function canonical(p) {
14654
14724
  return path29.resolve(p);
14655
14725
  }
14656
14726
  }
14657
- async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append) {
14727
+ async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append, scopeProject, projectConfigRoot) {
14658
14728
  const here = canonical(cwd);
14659
14729
  const notice = `\u{1F39A}\uFE0F effort \u2192 ${effort}: ${EFFORT_MEANING[effort]}`;
14660
14730
  for (const [projName, proj] of Object.entries(config.projects)) {
14661
14731
  if (canonical(proj.path) === here) continue;
14732
+ if (scopeProject) {
14733
+ if (projName !== scopeProject) continue;
14734
+ } else if (loadProjectOverride(projName, projectConfigRoot).effort !== void 0) {
14735
+ continue;
14736
+ }
14662
14737
  try {
14663
14738
  const ref = await driver.status(proj.captainName);
14664
14739
  if (ref) {
@@ -14668,22 +14743,28 @@ async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(
14668
14743
  }
14669
14744
  }
14670
14745
  }
14671
- var effortCommand = new Command28("effort").description("Get or set the global crew tokenomics dial (max | balance | low)").argument("[value]", "effort level to set: max | balance | low").option("--project <name>", "show resolved effort for a specific project").action(async (value, options) => {
14746
+ var effortCommand = new Command28("effort").description("Get or set the crew tokenomics dial (max | balance | low) \u2014 global by default, or per-project with --project").argument("[value]", "effort level to set: max | balance | low").option("--project <name>", "target a specific project (get: show its resolved effort; set: write a per-project override)").action(async (value, options) => {
14672
14747
  if (value === void 0) {
14673
- const { effort: effort2, description } = runEffortGet(void 0, options.project);
14748
+ let result;
14749
+ try {
14750
+ result = runEffortGet(void 0, options.project);
14751
+ } catch (err) {
14752
+ console.error(chalk28.red(err.message));
14753
+ process.exit(1);
14754
+ }
14674
14755
  const label = options.project ? `${options.project} project` : "global";
14675
- console.log(chalk28.bold(`Current effort (${label}):`), chalk28.cyan(effort2));
14676
- console.log(chalk28.dim(EFFORT_MEANING[effort2]));
14756
+ console.log(chalk28.bold(`Current effort (${label}):`), chalk28.cyan(result.effort));
14757
+ console.log(chalk28.dim(EFFORT_MEANING[result.effort]));
14677
14758
  return;
14678
14759
  }
14679
14760
  try {
14680
- runEffortSet(value);
14761
+ runEffortSet(value, void 0, options.project);
14681
14762
  } catch (err) {
14682
14763
  console.error(chalk28.red(err.message));
14683
14764
  process.exit(1);
14684
14765
  }
14685
14766
  const effort = value;
14686
- console.log(chalk28.green(`\u2714 effort \u2192 ${effort}`));
14767
+ console.log(chalk28.green(`\u2714 effort \u2192 ${effort} (${effortScopeLabel(options.project)})`));
14687
14768
  console.log(chalk28.dim(EFFORT_MEANING[effort]));
14688
14769
  try {
14689
14770
  const { createCmuxDriver: createCmuxDriver2, RuntimeRegistry: RuntimeRegistry2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports3));
@@ -14692,7 +14773,7 @@ var effortCommand = new Command28("effort").description("Get or set the global c
14692
14773
  const driver = registry.global(config);
14693
14774
  const stateRoot = path29.join(path29.dirname(DEFAULT_CONFIG_PATH), "state");
14694
14775
  const append = (project, text) => appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
14695
- await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append);
14776
+ await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append, options.project);
14696
14777
  } catch {
14697
14778
  console.log(chalk28.dim("(no running captain detected \u2014 change applies on next launch)"));
14698
14779
  }
@@ -15048,10 +15129,8 @@ function mapHookSub(sub, payload, taskId) {
15048
15129
  return mapClaudeHookToEvent("Stop", payload, taskId);
15049
15130
  case "notification":
15050
15131
  return mapClaudeHookToEvent("Notification", payload, taskId);
15051
- case "ask-question": {
15052
- const q = typeof payload?.question === "string" ? payload.question : "awaiting input";
15053
- return { type: "task.input.requested", id: taskId, requestId: 0, question: q };
15054
- }
15132
+ case "ask-question":
15133
+ return mapClaudeHookToEvent("PreToolUse", payload, taskId);
15055
15134
  case "session-end":
15056
15135
  return mapClaudeHookToEvent("SessionEnd", payload, taskId);
15057
15136
  default: