squadrant 0.16.2 → 0.16.4

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
@@ -2332,13 +2332,14 @@ function projectHealth(input) {
2332
2332
  const { project, now, captainName, captainStopped, commandPresent, crews } = input;
2333
2333
  const out = [];
2334
2334
  const captainState = input.captainState ?? (captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown");
2335
+ const deferral = input.captainDeferral;
2335
2336
  out.push({
2336
2337
  kind: "captain",
2337
2338
  project,
2338
2339
  ref: captainName,
2339
2340
  state: captainState,
2340
2341
  lastSeenMs: null,
2341
- detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : void 0
2342
+ detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries) \u2014 draft/ghost text blocking captain pane; input never touched, delivers automatically once cleared` : void 0
2342
2343
  });
2343
2344
  if (commandPresent !== null) {
2344
2345
  out.push({
@@ -3005,6 +3006,8 @@ function buildContext(opts) {
3005
3006
  opencodeBridge: null,
3006
3007
  cmuxEventsBridge: null,
3007
3008
  telegramBridge: void 0,
3009
+ notifyFault: opts.notifyFault ?? (() => {
3010
+ }),
3008
3011
  lifecycleSources: opts.lifecycleSources ?? [],
3009
3012
  broadcast: () => {
3010
3013
  },
@@ -3345,7 +3348,7 @@ var init_captain_delivery = __esm({
3345
3348
  const seq = entry.seq;
3346
3349
  const deferCount = this.deferCounts.get(seq) ?? 0;
3347
3350
  const stable = (this.stableCounts.get(seq) ?? 0) >= this.opts.stableProbePolls;
3348
- const probe = stable || deferCount >= this.opts.maxDefers;
3351
+ const probe = stable;
3349
3352
  try {
3350
3353
  await send(msg, probe ? { probe: true } : void 0);
3351
3354
  this.deferCounts.delete(seq);
@@ -3469,7 +3472,9 @@ async function runLivenessTick(deps) {
3469
3472
  }
3470
3473
  }
3471
3474
  function createDelivery(ctx, daemonCmux) {
3472
- const { stateRoot, store, log, livenessRegistry, isPidAlive, opts } = ctx;
3475
+ const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
3476
+ const notifyFault = ctx.notifyFault ?? (() => {
3477
+ });
3473
3478
  const defaultNotify = async (args) => {
3474
3479
  try {
3475
3480
  await appendToMailbox({
@@ -3492,6 +3497,7 @@ function createDelivery(ctx, daemonCmux) {
3492
3497
  const cfg = loadConfig();
3493
3498
  const deliveries = /* @__PURE__ */ new Map();
3494
3499
  const deliveryStats = (project) => deliveries.get(project)?.stats();
3500
+ const stuckNotified = /* @__PURE__ */ new Set();
3495
3501
  const sessionStartMs = Date.now();
3496
3502
  let delivering = false;
3497
3503
  const deliveryCore = async () => {
@@ -3563,6 +3569,18 @@ function createDelivery(ctx, daemonCmux) {
3563
3569
  break;
3564
3570
  }
3565
3571
  }
3572
+ const stuck = d.stats().stuck;
3573
+ if (stuck && !stuckNotified.has(project)) {
3574
+ stuckNotified.add(project);
3575
+ const { maxDeferCount } = d.stats();
3576
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
3577
+ const text = `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
3578
+ appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
3579
+ Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
3580
+ telegramBridge?.pushRaw(project, text);
3581
+ } else if (!stuck && stuckNotified.has(project)) {
3582
+ stuckNotified.delete(project);
3583
+ }
3566
3584
  }
3567
3585
  };
3568
3586
  const deliveryTick = async () => {
@@ -3839,7 +3857,12 @@ function startDaemon(ctx, opts, pkgVersion) {
3839
3857
  captainStopped: null,
3840
3858
  captainState: deriveCaptainState(capEntry),
3841
3859
  commandPresent: null,
3842
- crews: store.list(project)
3860
+ crews: store.list(project),
3861
+ // #579/#484 Gap 3: surface the same deferral stats already exposed to
3862
+ // the snapshot (line ~135 below) on the health row too, so `squadrant
3863
+ // doctor` / `squadrant status --detailed` show a stuck delivery with
3864
+ // zero configuration.
3865
+ captainDeferral: deliveryStats(project)
3843
3866
  }));
3844
3867
  }
3845
3868
  return out;
@@ -4740,6 +4763,14 @@ function createTelegramBridge(opts) {
4740
4763
  s.offset = next;
4741
4764
  saveState(stateRoot, s);
4742
4765
  }
4766
+ async function sendToTopic(project, text) {
4767
+ let threadId = loadState(stateRoot).topics[topicKey(project)];
4768
+ if (threadId === void 0) {
4769
+ threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
4770
+ setTopic(stateRoot, project, threadId);
4771
+ }
4772
+ await client.sendMessage(cfg.supergroupId, threadId, text);
4773
+ }
4743
4774
  async function deliverOutbound(project, ev) {
4744
4775
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
4745
4776
  const live = loadState(stateRoot).notify[project];
@@ -4748,12 +4779,10 @@ function createTelegramBridge(opts) {
4748
4779
  return;
4749
4780
  if (!tierIncludes(resolved.crew, ev.type))
4750
4781
  return;
4751
- let threadId = loadState(stateRoot).topics[topicKey(project)];
4752
- if (threadId === void 0) {
4753
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
4754
- setTopic(stateRoot, project, threadId);
4755
- }
4756
- await client.sendMessage(cfg.supergroupId, threadId, formatLifecycle(project, ev));
4782
+ await sendToTopic(project, formatLifecycle(project, ev));
4783
+ }
4784
+ async function deliverRawOutbound(project, text) {
4785
+ await sendToTopic(project, text);
4757
4786
  }
4758
4787
  function resolveLiveNotify(project) {
4759
4788
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -5061,6 +5090,11 @@ function createTelegramBridge(opts) {
5061
5090
  log(`telegram outbound failed project=${project}: ${e.message}`);
5062
5091
  });
5063
5092
  },
5093
+ pushRaw(project, text) {
5094
+ void deliverRawOutbound(project, text).catch((e) => {
5095
+ log(`telegram raw push failed project=${project}: ${e.message}`);
5096
+ });
5097
+ },
5064
5098
  health() {
5065
5099
  return { polling: running, lastSuccessfulPollAt, lastError, lastErrorAt };
5066
5100
  }
@@ -5880,6 +5914,9 @@ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
5880
5914
  }
5881
5915
  return { ...pane, title };
5882
5916
  }
5917
+ function pickMostRecentTask(tasks) {
5918
+ return tasks.reduce((a, b) => (b.createdAt ?? 0) > (a.createdAt ?? 0) ? b : a);
5919
+ }
5883
5920
  async function runCrewSend(project, name, message, runtime, workspaceId, deps) {
5884
5921
  const crew = await findCrewPane(runtime, workspaceId, project, name);
5885
5922
  if (!crew) {
@@ -5890,8 +5927,8 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps) {
5890
5927
  throw new Error(blockedByModalMessage());
5891
5928
  }
5892
5929
  try {
5893
- const tasks = await deps.listTasks(project);
5894
- const task = tasks.find((t) => t.name === name);
5930
+ const matches = (await deps.listTasks(project)).filter((t) => t.name === name);
5931
+ const task = matches.length > 0 ? pickMostRecentTask(matches) : void 0;
5895
5932
  if (task) {
5896
5933
  if (TERMINAL_STATES.has(task.state)) {
5897
5934
  await deps.emitEvent(project, { type: "task.reopened", id: task.id });
@@ -5929,7 +5966,7 @@ async function runCrewClose(project, name, runtime, workspaceId, deps) {
5929
5966
  matches = (await deps.listTasks(project)).filter((t) => t.name === name);
5930
5967
  }
5931
5968
  if (matches.length > 0) {
5932
- const primary = matches.reduce((a, b) => (b.createdAt ?? 0) > (a.createdAt ?? 0) ? b : a);
5969
+ const primary = pickMostRecentTask(matches);
5933
5970
  taskId = primary.id;
5934
5971
  if (primary.cwd && projRoot && primary.cwd !== projRoot) {
5935
5972
  worktreeCwd = primary.cwd;
@@ -6735,7 +6772,8 @@ var init_runtimes = __esm({
6735
6772
  });
6736
6773
 
6737
6774
  // packages/workspaces/dist/notifiers/cmux.js
6738
- import { execFileSync as execFileSync6, execSync as execSync2 } from "child_process";
6775
+ import { execFile as execFileCb, execSync as execSync2 } from "child_process";
6776
+ import { promisify as promisify2 } from "util";
6739
6777
  function createCmuxNotifier(_scope) {
6740
6778
  return {
6741
6779
  name: "cmux",
@@ -6752,13 +6790,15 @@ function createCmuxNotifier(_scope) {
6752
6790
  }
6753
6791
  },
6754
6792
  async notify(message) {
6755
- execFileSync6("squadrant", ["runtime", "send", "--command", message], { encoding: "utf-8", timeout: CMUX_TIMEOUT });
6793
+ await execFile3("squadrant", ["runtime", "send", "--command", message], { encoding: "utf-8", timeout: CMUX_TIMEOUT });
6756
6794
  }
6757
6795
  };
6758
6796
  }
6797
+ var execFile3;
6759
6798
  var init_cmux2 = __esm({
6760
6799
  "packages/workspaces/dist/notifiers/cmux.js"() {
6761
6800
  init_cmux();
6801
+ execFile3 = promisify2(execFileCb);
6762
6802
  }
6763
6803
  });
6764
6804
 
@@ -9341,16 +9381,23 @@ function isPermissionNotification(message) {
9341
9381
  const lower = message.toLowerCase();
9342
9382
  return lower.includes("permission") || lower.includes("approve");
9343
9383
  }
9384
+ function installHookEntry(hooks, event, matcher, command) {
9385
+ if (!Array.isArray(hooks[event]))
9386
+ hooks[event] = [];
9387
+ const entries = hooks[event];
9388
+ const already = entries.some((m) => m?.matcher === matcher && Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h?.command === "string" && h.command.includes(command)));
9389
+ if (!already) {
9390
+ entries.push({ matcher, hooks: [{ type: "command", command, timeout: 10 }] });
9391
+ }
9392
+ }
9344
9393
  function mergeClaudeHooks(settings, hookCmd) {
9345
9394
  const next = structuredClone(settings ?? {});
9346
9395
  next.hooks ??= {};
9347
9396
  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
- }
9397
+ installHookEntry(next.hooks, ev, "", `${hookCmd} ${ev}`);
9398
+ }
9399
+ for (const [ev, matcher] of MATCHED_EVENTS) {
9400
+ installHookEntry(next.hooks, ev, matcher, `${hookCmd} ${ev}`);
9354
9401
  }
9355
9402
  return next;
9356
9403
  }
@@ -9430,8 +9477,32 @@ function resolveLastAssistantText(payload) {
9430
9477
  }
9431
9478
  return null;
9432
9479
  }
9480
+ function formatAskUserQuestionPrompt(toolInput) {
9481
+ const questions = toolInput?.questions;
9482
+ if (!Array.isArray(questions) || questions.length === 0)
9483
+ return null;
9484
+ const parts = [];
9485
+ for (const q of questions) {
9486
+ if (!q || typeof q !== "object")
9487
+ continue;
9488
+ const text = q.question;
9489
+ if (typeof text !== "string" || !text.trim())
9490
+ continue;
9491
+ const options = Array.isArray(q.options) ? q.options : [];
9492
+ const labels = options.map((o) => o && typeof o.label === "string" ? o.label.trim() : null).filter((l) => !!l);
9493
+ parts.push(labels.length > 0 ? `${text.trim()} (options: ${labels.join(", ")})` : text.trim());
9494
+ }
9495
+ return parts.length > 0 ? parts.join(" | ") : null;
9496
+ }
9433
9497
  function mapClaudeHookToEvent(event, payload, taskId) {
9434
9498
  switch (event) {
9499
+ case "PreToolUse": {
9500
+ const toolName = payload?.tool_name;
9501
+ if (toolName !== "AskUserQuestion")
9502
+ return null;
9503
+ const question = formatAskUserQuestionPrompt(payload?.tool_input) ?? "crew opened an AskUserQuestion prompt (options unavailable)";
9504
+ return { type: "task.input.requested", id: taskId, requestId: nextAskUserQuestionRequestId++, question };
9505
+ }
9435
9506
  case "Stop": {
9436
9507
  const text = resolveLastAssistantText(payload);
9437
9508
  const question = text ? detectTrailingQuestion2(text) : null;
@@ -9458,10 +9529,14 @@ function mapClaudeHookToEvent(event, payload, taskId) {
9458
9529
  return null;
9459
9530
  }
9460
9531
  }
9461
- var EVENTS, claudeInteractive;
9532
+ var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, claudeInteractive;
9462
9533
  var init_claude2 = __esm({
9463
9534
  "packages/agents/dist/interactive/claude.js"() {
9464
9535
  EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit"];
9536
+ MATCHED_EVENTS = [
9537
+ ["PreToolUse", "AskUserQuestion"]
9538
+ ];
9539
+ nextAskUserQuestionRequestId = Date.now();
9465
9540
  claudeInteractive = {
9466
9541
  provider: "claude",
9467
9542
  tier: "strong",
@@ -9814,6 +9889,7 @@ __export(dist_exports4, {
9814
9889
  createOpencodeEmitter: () => createOpencodeEmitter,
9815
9890
  deriveTranscriptPath: () => deriveTranscriptPath,
9816
9891
  detectTrailingQuestion: () => detectTrailingQuestion2,
9892
+ formatAskUserQuestionPrompt: () => formatAskUserQuestionPrompt,
9817
9893
  getHeadlessAdapter: () => getHeadlessAdapter,
9818
9894
  getInteractiveAdapter: () => getInteractiveAdapter,
9819
9895
  isPermissionNotification: () => isPermissionNotification,
@@ -10760,9 +10836,18 @@ function progressBar(completed, total) {
10760
10836
  }
10761
10837
  function captainIndicator(state) {
10762
10838
  if (state === "alive" || state === "stale") return chalk6.green("\u25CF");
10839
+ if (state === "stopped") return chalk6.magenta("\u23FB");
10763
10840
  if (state === void 0 || state === "unknown") return chalk6.dim("?");
10764
10841
  return chalk6.dim("\u25CB");
10765
10842
  }
10843
+ function formatProjectRow(name, captainName, fm, statusMdState, captainState) {
10844
+ const sessionIndicator = captainIndicator(captainState);
10845
+ const captainDisplay = `${captainName.padEnd(11)} ${sessionIndicator}`;
10846
+ const crew = statusMdState === "ok" ? String(fm.active_crew ?? 0).padEnd(6) : chalk6.dim("?").padEnd(6);
10847
+ const progress = statusMdState === "ok" ? progressBar(fm.tasks_completed ?? 0, fm.tasks_total ?? 0).padEnd(25) : statusMdState === "unreadable" ? chalk6.red("status.md unreadable").padEnd(25) : chalk6.dim("no notes").padEnd(25);
10848
+ const updated = statusMdState === "ok" ? timeAgo(fm.last_updated) : chalk6.dim("\u2014");
10849
+ return ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`;
10850
+ }
10766
10851
  var statusCommand = new Command4("status").description("Show status of all projects from spoke vault status files").option("--detailed", "also show live per-component service health from the daemon (#77)").action(async (opts) => {
10767
10852
  const config = loadConfig();
10768
10853
  const projects = Object.entries(config.projects);
@@ -10787,28 +10872,19 @@ var statusCommand = new Command4("status").description("Show status of all proje
10787
10872
  console.log(chalk6.dim(" " + "\u2500".repeat(85)));
10788
10873
  for (const [name, project] of projects) {
10789
10874
  const workspace = registry.forProject(name, config);
10790
- if (!await workspace.exists("status.md")) {
10791
- console.log(` ${name.padEnd(18)} ${chalk6.dim("no status.md")}`);
10792
- continue;
10793
- }
10794
10875
  let fm = {};
10795
- try {
10796
- const raw = await workspace.read("status.md");
10797
- fm = matter2(raw).data;
10798
- } catch {
10799
- console.log(` ${name.padEnd(18)} ${chalk6.red("error reading status.md")}`);
10800
- continue;
10876
+ let statusMdState = "missing";
10877
+ if (await workspace.exists("status.md")) {
10878
+ try {
10879
+ const raw = await workspace.read("status.md");
10880
+ fm = matter2(raw).data;
10881
+ statusMdState = "ok";
10882
+ } catch {
10883
+ statusMdState = "unreadable";
10884
+ }
10801
10885
  }
10802
- const sessionIndicator = captainIndicator(captainStateByProject.get(name));
10803
- const captainDisplay = `${project.captainName.padEnd(11)} ${sessionIndicator}`;
10804
- const crew = String(fm.active_crew ?? 0).padEnd(6);
10805
- const progress = progressBar(
10806
- fm.tasks_completed ?? 0,
10807
- fm.tasks_total ?? 0
10808
- ).padEnd(25);
10809
- const updated = timeAgo(fm.last_updated);
10810
10886
  console.log(
10811
- ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`
10887
+ formatProjectRow(name, project.captainName, fm, statusMdState, captainStateByProject.get(name))
10812
10888
  );
10813
10889
  }
10814
10890
  console.log("");
@@ -11310,6 +11386,22 @@ function defaultWriteResult(id, payload) {
11310
11386
  writeFileSync10(file, payload);
11311
11387
  return file;
11312
11388
  }
11389
+ async function runCrewSignal(signal, o, deps) {
11390
+ const taskId = o.taskId ?? process.env.SQUADRANT_CREW_TASK_ID;
11391
+ const project = o.project ?? process.env.SQUADRANT_CREW_PROJECT;
11392
+ if (!taskId)
11393
+ throw new Error("not running under a crew (SQUADRANT_CREW_TASK_ID unset)");
11394
+ if (!project)
11395
+ throw new Error("not running under a crew (SQUADRANT_CREW_PROJECT unset)");
11396
+ const current = await deps.call(buildStatusRequest(project, taskId));
11397
+ if (current && TERMINAL_STATES.has(current.state)) {
11398
+ throw new Error(
11399
+ `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.`
11400
+ );
11401
+ }
11402
+ const req = buildSignalRequest(signal, { ...o, writeResult: o.writeResult ?? defaultWriteResult });
11403
+ await deps.call(req);
11404
+ }
11313
11405
  function addControlPlaneCrewCommands(crew) {
11314
11406
  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
11407
  const req = buildDispatchRequest({ project, task, provider: opts.provider, mode: opts.mode, cwd: opts.cwd });
@@ -11391,15 +11483,14 @@ function addControlPlaneCrewCommands(crew) {
11391
11483
  process.exit(2);
11392
11484
  }
11393
11485
  try {
11394
- const req = buildSignalRequest(state, {
11486
+ await runCrewSignal(state, {
11395
11487
  ...opts.message !== void 0 ? { message: opts.message } : {},
11396
11488
  ...opts.question !== void 0 ? { question: opts.question } : {},
11397
11489
  ...opts.error !== void 0 ? { error: opts.error } : {},
11398
11490
  ...opts.taskId !== void 0 ? { taskId: opts.taskId } : {},
11399
11491
  ...opts.project !== void 0 ? { project: opts.project } : {},
11400
11492
  writeResult: defaultWriteResult
11401
- });
11402
- await squadrantdCall(req);
11493
+ }, { call: squadrantdCall });
11403
11494
  process.exit(0);
11404
11495
  } catch (e) {
11405
11496
  process.stderr.write(`${e.message}
@@ -11928,7 +12019,7 @@ init_dist();
11928
12019
  import { join as join22 } from "path";
11929
12020
  import { homedir as homedir17 } from "os";
11930
12021
  import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
11931
- import { execFile as execFile3 } from "child_process";
12022
+ import { execFile as execFile4 } from "child_process";
11932
12023
  var DEFAULT_TIMEOUT_MS = 2e3;
11933
12024
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
11934
12025
  function withTimeout2(p, ms) {
@@ -12045,7 +12136,7 @@ function defaultProbeRunners() {
12045
12136
  return {
12046
12137
  probeCmuxBin: () => new Promise((resolve3) => {
12047
12138
  try {
12048
- execFile3(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
12139
+ execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
12049
12140
  } catch {
12050
12141
  resolve3(false);
12051
12142
  }
@@ -14631,22 +14722,38 @@ var EFFORT_MEANING = {
14631
14722
  balance: "normal routing \u2014 use default crew routing rules unchanged",
14632
14723
  low: "conserve tokens \u2014 bias crew spawns toward opencode/sonnet; reserve opus for work that genuinely needs it"
14633
14724
  };
14634
- function runEffortGet(configPath = DEFAULT_CONFIG_PATH, projectName) {
14725
+ function runEffortGet(configPath = DEFAULT_CONFIG_PATH, projectName, projectConfigRoot) {
14635
14726
  const config = loadConfig(configPath);
14636
- const effort = resolveEffort(config, projectName);
14727
+ if (projectName && !(projectName in config.projects)) {
14728
+ const known = Object.keys(config.projects).sort().join(", ") || "(no projects registered)";
14729
+ throw new Error(`Unknown project '${projectName}'. Known projects: ${known}`);
14730
+ }
14731
+ const effort = resolveEffort(config, projectName, projectConfigRoot);
14637
14732
  const description = `${effort}: ${EFFORT_MEANING[effort]}`;
14638
14733
  return { effort, description };
14639
14734
  }
14640
- function runEffortSet(value, configPath = DEFAULT_CONFIG_PATH) {
14735
+ function runEffortSet(value, configPath = DEFAULT_CONFIG_PATH, projectName, projectConfigRoot) {
14641
14736
  if (!VALID_EFFORTS.includes(value)) {
14642
14737
  throw new Error(
14643
14738
  `Invalid effort '${value}'. Valid values: ${VALID_EFFORTS.join(" | ")}`
14644
14739
  );
14645
14740
  }
14741
+ if (projectName) {
14742
+ const config2 = loadConfig(configPath);
14743
+ if (!(projectName in config2.projects)) {
14744
+ const known = Object.keys(config2.projects).sort().join(", ") || "(no projects registered)";
14745
+ throw new Error(`Unknown project '${projectName}'. Known projects: ${known}`);
14746
+ }
14747
+ saveProjectOverride(projectName, { effort: value }, projectConfigRoot);
14748
+ return;
14749
+ }
14646
14750
  const config = loadConfig(configPath);
14647
14751
  config.defaults.effort = value;
14648
14752
  saveConfig(config, configPath);
14649
14753
  }
14754
+ function effortScopeLabel(projectName) {
14755
+ return projectName ? `project: ${projectName}` : "global";
14756
+ }
14650
14757
  function canonical(p) {
14651
14758
  try {
14652
14759
  return fs28.realpathSync(p);
@@ -14654,11 +14761,16 @@ function canonical(p) {
14654
14761
  return path29.resolve(p);
14655
14762
  }
14656
14763
  }
14657
- async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append) {
14764
+ async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append, scopeProject, projectConfigRoot) {
14658
14765
  const here = canonical(cwd);
14659
14766
  const notice = `\u{1F39A}\uFE0F effort \u2192 ${effort}: ${EFFORT_MEANING[effort]}`;
14660
14767
  for (const [projName, proj] of Object.entries(config.projects)) {
14661
14768
  if (canonical(proj.path) === here) continue;
14769
+ if (scopeProject) {
14770
+ if (projName !== scopeProject) continue;
14771
+ } else if (loadProjectOverride(projName, projectConfigRoot).effort !== void 0) {
14772
+ continue;
14773
+ }
14662
14774
  try {
14663
14775
  const ref = await driver.status(proj.captainName);
14664
14776
  if (ref) {
@@ -14668,22 +14780,28 @@ async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(
14668
14780
  }
14669
14781
  }
14670
14782
  }
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) => {
14783
+ 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
14784
  if (value === void 0) {
14673
- const { effort: effort2, description } = runEffortGet(void 0, options.project);
14785
+ let result;
14786
+ try {
14787
+ result = runEffortGet(void 0, options.project);
14788
+ } catch (err) {
14789
+ console.error(chalk28.red(err.message));
14790
+ process.exit(1);
14791
+ }
14674
14792
  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]));
14793
+ console.log(chalk28.bold(`Current effort (${label}):`), chalk28.cyan(result.effort));
14794
+ console.log(chalk28.dim(EFFORT_MEANING[result.effort]));
14677
14795
  return;
14678
14796
  }
14679
14797
  try {
14680
- runEffortSet(value);
14798
+ runEffortSet(value, void 0, options.project);
14681
14799
  } catch (err) {
14682
14800
  console.error(chalk28.red(err.message));
14683
14801
  process.exit(1);
14684
14802
  }
14685
14803
  const effort = value;
14686
- console.log(chalk28.green(`\u2714 effort \u2192 ${effort}`));
14804
+ console.log(chalk28.green(`\u2714 effort \u2192 ${effort} (${effortScopeLabel(options.project)})`));
14687
14805
  console.log(chalk28.dim(EFFORT_MEANING[effort]));
14688
14806
  try {
14689
14807
  const { createCmuxDriver: createCmuxDriver2, RuntimeRegistry: RuntimeRegistry2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports3));
@@ -14692,7 +14810,7 @@ var effortCommand = new Command28("effort").description("Get or set the global c
14692
14810
  const driver = registry.global(config);
14693
14811
  const stateRoot = path29.join(path29.dirname(DEFAULT_CONFIG_PATH), "state");
14694
14812
  const append = (project, text) => appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
14695
- await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append);
14813
+ await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append, options.project);
14696
14814
  } catch {
14697
14815
  console.log(chalk28.dim("(no running captain detected \u2014 change applies on next launch)"));
14698
14816
  }
@@ -15048,10 +15166,8 @@ function mapHookSub(sub, payload, taskId) {
15048
15166
  return mapClaudeHookToEvent("Stop", payload, taskId);
15049
15167
  case "notification":
15050
15168
  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
- }
15169
+ case "ask-question":
15170
+ return mapClaudeHookToEvent("PreToolUse", payload, taskId);
15055
15171
  case "session-end":
15056
15172
  return mapClaudeHookToEvent("SessionEnd", payload, taskId);
15057
15173
  default: