pi-crew 0.9.20 → 0.9.22

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.mjs CHANGED
@@ -11703,6 +11703,9 @@ function resetEventLogMode() {
11703
11703
  asyncQueues.clear();
11704
11704
  }
11705
11705
  async function appendEventAsync(eventsPath, event) {
11706
+ if (!TERMINAL_EVENT_TYPES.has(event.type)) {
11707
+ return appendEventBuffered(eventsPath, event);
11708
+ }
11706
11709
  const queueKey = eventsPath;
11707
11710
  const prev = asyncQueues.get(queueKey) ?? Promise.resolve();
11708
11711
  const next = prev.then(async () => {
@@ -11853,6 +11856,101 @@ async function appendEventAsync(eventsPath, event) {
11853
11856
  );
11854
11857
  return next;
11855
11858
  }
11859
+ async function appendEventBatchInsideLock(eventsPath, queue) {
11860
+ if (queue.length === 0) return;
11861
+ fs8.mkdirSync(path6.dirname(eventsPath), { recursive: true });
11862
+ try {
11863
+ if (fs8.existsSync(eventsPath)) {
11864
+ const stat2 = fs8.statSync(eventsPath);
11865
+ if (stat2.size > MAX_EVENTS_BYTES) {
11866
+ try {
11867
+ const prepared = prepareCompaction(eventsPath);
11868
+ if (prepared) applyCompactionUnlocked(eventsPath, prepared);
11869
+ } catch (error) {
11870
+ logInternalError("event-log.batch-immediate-compact", error, `eventsPath=${eventsPath}`);
11871
+ }
11872
+ if (fs8.existsSync(eventsPath) && fs8.statSync(eventsPath).size > MAX_EVENTS_BYTES) {
11873
+ rotateEventLogUnlocked(eventsPath);
11874
+ }
11875
+ }
11876
+ }
11877
+ } catch (error) {
11878
+ logInternalError("event-log.batch-size-check", error, `eventsPath=${eventsPath}`);
11879
+ }
11880
+ const startingSeq = queue[0]?.event.metadata?.seq ?? nextSequence(eventsPath);
11881
+ let nextSeq = startingSeq;
11882
+ const finalized = [];
11883
+ let lastSeq = 0;
11884
+ for (const item of queue) {
11885
+ const baseMetadata = item.event.metadata;
11886
+ const seq = baseMetadata?.seq ?? nextSeq++;
11887
+ let metadata = {
11888
+ seq,
11889
+ provenance: baseMetadata?.provenance ?? "team_runner",
11890
+ ...baseMetadata?.parentEventId ? { parentEventId: baseMetadata.parentEventId } : {},
11891
+ ...baseMetadata?.attemptId ? { attemptId: baseMetadata.attemptId } : {},
11892
+ ...baseMetadata?.branchId ? { branchId: baseMetadata.branchId } : {},
11893
+ ...baseMetadata?.causationId ? { causationId: baseMetadata.causationId } : {},
11894
+ ...baseMetadata?.correlationId ? { correlationId: baseMetadata.correlationId } : {},
11895
+ ...baseMetadata?.sessionIdentity ? { sessionIdentity: baseMetadata.sessionIdentity } : {},
11896
+ ...baseMetadata?.ownership ? { ownership: baseMetadata.ownership } : {},
11897
+ ...baseMetadata?.nudgeId ? { nudgeId: baseMetadata.nudgeId } : {},
11898
+ ...baseMetadata?.confidence ? { confidence: baseMetadata.confidence } : {}
11899
+ };
11900
+ const fullEvent = {
11901
+ time: (/* @__PURE__ */ new Date()).toISOString(),
11902
+ ...item.event,
11903
+ metadata
11904
+ };
11905
+ if (baseMetadata?.fingerprint || TERMINAL_EVENT_TYPES.has(fullEvent.type)) {
11906
+ metadata = {
11907
+ ...metadata,
11908
+ fingerprint: baseMetadata?.fingerprint ?? computeEventFingerprint(fullEvent)
11909
+ };
11910
+ fullEvent.metadata = metadata;
11911
+ }
11912
+ finalized.push({ item, line: `${JSON.stringify(redactSecrets(fullEvent))}
11913
+ `, fullEvent });
11914
+ lastSeq = seq;
11915
+ }
11916
+ try {
11917
+ if (fs8.existsSync(eventsPath) && fs8.statSync(eventsPath).size > MAX_EVENTS_BYTES) {
11918
+ logInternalError(
11919
+ "event-log.size-limit",
11920
+ new Error(`events file ${eventsPath} exceeds ${MAX_EVENTS_BYTES} bytes after compaction`),
11921
+ `eventsPath=${eventsPath}`
11922
+ );
11923
+ for (const { item } of finalized) item.reject(new Error("event log size limit exceeded"));
11924
+ return;
11925
+ }
11926
+ } catch (error) {
11927
+ logInternalError("event-log.batch-size-check-post", error, `eventsPath=${eventsPath}`);
11928
+ }
11929
+ fs8.appendFileSync(eventsPath, finalized.map((f) => f.line).join(""), "utf-8");
11930
+ const fd = fs8.openSync(eventsPath, "r+");
11931
+ try {
11932
+ fs8.fsyncSync(fd);
11933
+ } catch {
11934
+ } finally {
11935
+ fs8.closeSync(fd);
11936
+ }
11937
+ persistSequence(eventsPath, lastSeq);
11938
+ try {
11939
+ const stat2 = fs8.statSync(eventsPath);
11940
+ if (sequenceCache.size >= MAX_SEQUENCE_CACHE_ENTRIES) {
11941
+ evictOldestSequenceCacheEntries();
11942
+ }
11943
+ sequenceCache.set(eventsPath, {
11944
+ size: stat2.size,
11945
+ mtimeMs: stat2.mtimeMs,
11946
+ seq: lastSeq,
11947
+ lastAccessMs: Date.now()
11948
+ });
11949
+ } catch (error) {
11950
+ logInternalError("event-log.batch-cache-update", error, `eventsPath=${eventsPath}`);
11951
+ }
11952
+ for (const { item, fullEvent } of finalized) item.resolve(fullEvent);
11953
+ }
11856
11954
  function appendEventInsideLock(eventsPath, event) {
11857
11955
  fs8.mkdirSync(path6.dirname(eventsPath), { recursive: true });
11858
11956
  const baseMetadata = event.metadata;
@@ -11967,8 +12065,11 @@ function appendEventBuffered(eventsPath, event, bufferMs = DEFAULT_BUFFER_MS) {
11967
12065
  queue.push({ event, resolve: resolve21, reject });
11968
12066
  bufferedQueues.set(eventsPath, queue);
11969
12067
  if (!bufferedTimers.has(eventsPath)) {
11970
- const timer = setTimeout(() => flushOneEventLogBuffer(eventsPath), bufferMs);
11971
- timer.unref();
12068
+ const timer = setTimeout(() => {
12069
+ flushOneEventLogBuffer(eventsPath).catch((error) => {
12070
+ logInternalError("event-log.buffered-flush", error, `eventsPath=${eventsPath}`);
12071
+ });
12072
+ }, bufferMs);
11972
12073
  bufferedTimers.set(eventsPath, timer);
11973
12074
  }
11974
12075
  });
@@ -12000,14 +12101,7 @@ async function flushOneEventLogBuffer(eventsPath) {
12000
12101
  }
12001
12102
  }
12002
12103
  await withEventLogLockAsync(eventsPath, async () => {
12003
- for (const item of queue) {
12004
- try {
12005
- const ev = appendEventInsideLock(eventsPath, item.event);
12006
- item.resolve(ev);
12007
- } catch (error) {
12008
- item.reject(error);
12009
- }
12010
- }
12104
+ await appendEventBatchInsideLock(eventsPath, queue);
12011
12105
  });
12012
12106
  } catch (error) {
12013
12107
  if (queue) for (const item of queue) item.reject(error);
@@ -12108,8 +12202,14 @@ var init_event_log = __esm({
12108
12202
  bufferedTimers = /* @__PURE__ */ new Map();
12109
12203
  DEFAULT_BUFFER_MS = 20;
12110
12204
  process.on("exit", () => {
12111
- flushEventLogBuffer();
12112
- drainAsyncQueues();
12205
+ if (bufferedQueues.size > 0) {
12206
+ flushEventLogBuffer().catch(() => {
12207
+ });
12208
+ }
12209
+ if (asyncQueues.size > 0) {
12210
+ drainAsyncQueues().catch(() => {
12211
+ });
12212
+ }
12113
12213
  asyncQueues.clear();
12114
12214
  });
12115
12215
  process.on("SIGTERM", () => setImmediate(() => flushEventLogBuffer()));
@@ -17462,6 +17562,22 @@ var init_env_allowlist = __esm({
17462
17562
  function isKnownProviderKey(key) {
17463
17563
  return KNOWN_PROVIDER_KEYS.has(key);
17464
17564
  }
17565
+ function providerEnvKeys(modelId) {
17566
+ if (!modelId) return [];
17567
+ const separatorIndex = modelId.indexOf("/");
17568
+ if (separatorIndex <= 0) return [];
17569
+ const provider = modelId.substring(0, separatorIndex).toLowerCase();
17570
+ return PROVIDER_ENV_KEY_MAP[provider] ?? [];
17571
+ }
17572
+ function buildScopedAllowList(baseAllowList, models) {
17573
+ const providerKeys = /* @__PURE__ */ new Set();
17574
+ for (const model of models) {
17575
+ for (const key of providerEnvKeys(model)) {
17576
+ providerKeys.add(key);
17577
+ }
17578
+ }
17579
+ return [...baseAllowList, ...providerKeys];
17580
+ }
17465
17581
  function isDangerousGlob(pattern) {
17466
17582
  if (!pattern.endsWith("*")) return false;
17467
17583
  const prefix = pattern.slice(0, -1);
@@ -17502,7 +17618,7 @@ function sanitizeEnvSecrets(env, options) {
17502
17618
  }
17503
17619
  return filtered;
17504
17620
  }
17505
- var KNOWN_PROVIDER_KEYS, SECRET_SUFFIXES;
17621
+ var KNOWN_PROVIDER_KEYS, PROVIDER_ENV_KEY_MAP, SECRET_SUFFIXES;
17506
17622
  var init_env_filter = __esm({
17507
17623
  "src/utils/env-filter.ts"() {
17508
17624
  "use strict";
@@ -17523,6 +17639,19 @@ var init_env_filter = __esm({
17523
17639
  "ZEU_API_KEY",
17524
17640
  "ZERODEV_API_KEY"
17525
17641
  ]);
17642
+ PROVIDER_ENV_KEY_MAP = {
17643
+ minimax: ["MINIMAX_API_KEY", "MINIMAX_GROUP_ID"],
17644
+ openai: ["OPENAI_API_KEY", "OPENAI_ORG_ID"],
17645
+ anthropic: ["ANTHROPIC_API_KEY"],
17646
+ google: ["GOOGLE_API_KEY", "GOOGLE_GENERATIVE_LANGUAGE_API_KEY"],
17647
+ gemini: ["GOOGLE_API_KEY", "GOOGLE_GENERATIVE_LANGUAGE_API_KEY"],
17648
+ azure: ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"],
17649
+ "azure-openai": ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"],
17650
+ aws: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"],
17651
+ bedrock: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"],
17652
+ zai: ["ZEU_API_KEY"],
17653
+ zerodev: ["ZERODEV_API_KEY"]
17654
+ };
17526
17655
  SECRET_SUFFIXES = ["token", "api", "key", "password", "passwd", "secret", "credential", "authorization", "private"];
17527
17656
  }
17528
17657
  });
@@ -18194,7 +18323,7 @@ function terminateActiveChildPiProcesses() {
18194
18323
  for (const [pid, child] of entries) killProcessTree(pid, child);
18195
18324
  return entries.length;
18196
18325
  }
18197
- function buildChildPiSpawnOptions(cwd, env) {
18326
+ function buildChildPiSpawnOptions(cwd, env, model) {
18198
18327
  let validatedCwd;
18199
18328
  try {
18200
18329
  validatedCwd = fs27.realpathSync(cwd);
@@ -18209,81 +18338,8 @@ function buildChildPiSpawnOptions(cwd, env) {
18209
18338
  throw new Error(`Invalid cwd: ${cwd} \u2014 ${error instanceof Error ? error.message : String(error)}`);
18210
18339
  }
18211
18340
  }
18212
- const filteredEnv = sanitizeEnvSecrets(env, {
18213
- allowList: [
18214
- /*
18215
- * SECURITY WARNING: All model provider API keys below are passed to EVERY child worker.
18216
- * If any child is compromised (e.g. via prompt injection), all listed keys are exposed.
18217
- * This is a deliberate trade-off: multi-provider setups require the child Pi process to
18218
- * authenticate with whichever provider the model routes to. Reducing keys per-child
18219
- * would break multi-provider functionality. Mitigations:
18220
- * - sanitizeEnvSecrets strips all env vars NOT on this list.
18221
- * - Do NOT add wildcards ("*_API_KEY") — only explicit, intended provider keys.
18222
- * - Consider per-task key scoping if the architecture allows it in the future.
18223
- *
18224
- * MAINTENANCE REQUIREMENT: When new secret env vars are added to the Pi ecosystem,
18225
- * they MUST be explicitly added to this allowlist to be passed to child processes.
18226
- * A CI check should fail if a secret-like env var (matching patterns like *_API_KEY,
18227
- * *_TOKEN, *_SECRET) is detected in the codebase but not present in this list.
18228
- */
18229
- // NOTE: Model provider API keys are NOT needed here — child Pi uses the same
18230
- // config file as parent Pi. Passing keys via env is a security risk.
18231
- "PATH",
18232
- "HOME",
18233
- "USER",
18234
- "SHELL",
18235
- "TERM",
18236
- "LANG",
18237
- // FIX: Replaced broad wildcards (LC_*, XDG_*, NVM_*, NODE_*, npm_*) with
18238
- // specific names. Previously NPM_TOKEN, NODE_ENV=production, NVM_RC_VERSION
18239
- // all leaked through wildcards.
18240
- "LC_ALL",
18241
- "LC_COLLATE",
18242
- "LC_CTYPE",
18243
- "LC_MESSAGES",
18244
- "LC_MONETARY",
18245
- "LC_NUMERIC",
18246
- "LC_TIME",
18247
- "XDG_CONFIG_HOME",
18248
- "XDG_DATA_HOME",
18249
- "XDG_CACHE_HOME",
18250
- "XDG_RUNTIME_DIR",
18251
- // Windows essentials — see WINDOWS_ESSENTIAL_ENV_VARS (src/utils/env-allowlist.ts).
18252
- ...WINDOWS_ESSENTIAL_ENV_VARS,
18253
- "NVM_BIN",
18254
- "NVM_DIR",
18255
- "NVM_INC",
18256
- // NODE_PATH is intentionally omitted from the allowlist.
18257
- // NODE_PATH can reveal user environment information (e.g., NVM paths under $HOME)
18258
- // and the validation at lines 286-298 only filters to standard system prefixes.
18259
- // Removing it entirely is cleaner than best-effort filtering.
18260
- "NODE_DISABLE_COLORS",
18261
- "NODE_EXTRA_CA_CERTS",
18262
- "NPM_CONFIG_REGISTRY",
18263
- "NPM_CONFIG_USERCONFIG",
18264
- "NPM_CONFIG_GLOBALCONFIG",
18265
- // FIX: Replace PI_CREW_*/PI_TEAMS_* wildcards with explicit list of
18266
- // safe vars. Wildcards are fragile — any new secret var would leak.
18267
- // Only non-secret execution-control vars that children legitimately need.
18268
- "PI_CREW_DEPTH",
18269
- "PI_CREW_MAX_DEPTH",
18270
- "PI_CREW_INHERIT_PROJECT_CONTEXT",
18271
- "PI_CREW_INHERIT_SKILLS",
18272
- // PI_CREW_KIND marks this process as a crew sub-agent (vs the user's main session).
18273
- // doctor --zombies matches it to safely list orphaned sub-agents only.
18274
- "PI_CREW_KIND",
18275
- // PI_CREW_PARENT_PID is needed by child-pi's parent-guard (uses
18276
- // process.kill(pid, 0) liveness check). The PID is not a secret.
18277
- "PI_CREW_PARENT_PID",
18278
- "PI_TEAMS_DEPTH",
18279
- "PI_TEAMS_MAX_DEPTH",
18280
- "PI_TEAMS_INHERIT_PROJECT_CONTEXT",
18281
- "PI_TEAMS_INHERIT_SKILLS",
18282
- "PI_TEAMS_PI_BIN",
18283
- "PI_TEAMS_MOCK_CHILD_PI",
18284
- "PI_CREW_ALLOW_MOCK"
18285
- ]
18286
- });
18341
+ const allowList = model ? buildScopedAllowList(BASE_ALLOWLIST, [model]) : BASE_ALLOWLIST;
18342
+ const filteredEnv = sanitizeEnvSecrets(env, { allowList });
18287
18343
  if (filteredEnv.NODE_PATH) {
18288
18344
  const validPrefixes = ["/opt/", "/lib/", "/usr/local/", "/usr/", "/home/"];
18289
18345
  const validPaths = filteredEnv.NODE_PATH.split(":").filter((p) => {
@@ -18620,10 +18676,14 @@ ${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 1e-
18620
18676
  const child = spawn(
18621
18677
  spawnSpec.command,
18622
18678
  spawnSpec.args,
18623
- buildChildPiSpawnOptions(input.cwd, {
18624
- ...process.env,
18625
- ...built.env
18626
- })
18679
+ buildChildPiSpawnOptions(
18680
+ input.cwd,
18681
+ {
18682
+ ...process.env,
18683
+ ...built.env
18684
+ },
18685
+ input.model
18686
+ )
18627
18687
  );
18628
18688
  if (child.pid) {
18629
18689
  activeChildProcesses.set(child.pid, child);
@@ -19110,7 +19170,7 @@ ${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 1e-
19110
19170
  }
19111
19171
  }
19112
19172
  }
19113
- var POST_EXIT_STDIO_GUARD_MS, FINAL_DRAIN_MS, HARD_KILL_MS, RESPONSE_TIMEOUT_MS, MAX_CAPTURE_BYTES, MAX_ASSISTANT_TEXT_CHARS, MAX_TOOL_RESULT_CHARS, MAX_TOOL_INPUT_CHARS, MAX_COMPACT_CONTENT_CHARS, activeChildProcesses, childHardKillTimers, ChildPiLineObserver;
19173
+ var POST_EXIT_STDIO_GUARD_MS, FINAL_DRAIN_MS, HARD_KILL_MS, RESPONSE_TIMEOUT_MS, MAX_CAPTURE_BYTES, MAX_ASSISTANT_TEXT_CHARS, MAX_TOOL_RESULT_CHARS, MAX_TOOL_INPUT_CHARS, MAX_COMPACT_CONTENT_CHARS, activeChildProcesses, childHardKillTimers, BASE_ALLOWLIST, ChildPiLineObserver;
19114
19174
  var init_child_pi = __esm({
19115
19175
  "src/runtime/child-pi.ts"() {
19116
19176
  "use strict";
@@ -19139,6 +19199,48 @@ var init_child_pi = __esm({
19139
19199
  MAX_COMPACT_CONTENT_CHARS = DEFAULT_CHILD_PI.maxCompactContentChars;
19140
19200
  activeChildProcesses = /* @__PURE__ */ new Map();
19141
19201
  childHardKillTimers = /* @__PURE__ */ new Map();
19202
+ BASE_ALLOWLIST = [
19203
+ "PATH",
19204
+ "HOME",
19205
+ "USER",
19206
+ "SHELL",
19207
+ "TERM",
19208
+ "LANG",
19209
+ "LC_ALL",
19210
+ "LC_COLLATE",
19211
+ "LC_CTYPE",
19212
+ "LC_MESSAGES",
19213
+ "LC_MONETARY",
19214
+ "LC_NUMERIC",
19215
+ "LC_TIME",
19216
+ "XDG_CONFIG_HOME",
19217
+ "XDG_DATA_HOME",
19218
+ "XDG_CACHE_HOME",
19219
+ "XDG_RUNTIME_DIR",
19220
+ // Windows essentials — see WINDOWS_ESSENTIAL_ENV_VARS (src/utils/env-allowlist.ts).
19221
+ ...WINDOWS_ESSENTIAL_ENV_VARS,
19222
+ "NVM_BIN",
19223
+ "NVM_DIR",
19224
+ "NVM_INC",
19225
+ "NODE_DISABLE_COLORS",
19226
+ "NODE_EXTRA_CA_CERTS",
19227
+ "NPM_CONFIG_REGISTRY",
19228
+ "NPM_CONFIG_USERCONFIG",
19229
+ "NPM_CONFIG_GLOBALCONFIG",
19230
+ "PI_CREW_DEPTH",
19231
+ "PI_CREW_MAX_DEPTH",
19232
+ "PI_CREW_INHERIT_PROJECT_CONTEXT",
19233
+ "PI_CREW_INHERIT_SKILLS",
19234
+ "PI_CREW_KIND",
19235
+ "PI_CREW_PARENT_PID",
19236
+ "PI_TEAMS_DEPTH",
19237
+ "PI_TEAMS_MAX_DEPTH",
19238
+ "PI_TEAMS_INHERIT_PROJECT_CONTEXT",
19239
+ "PI_TEAMS_INHERIT_SKILLS",
19240
+ "PI_TEAMS_PI_BIN",
19241
+ "PI_TEAMS_MOCK_CHILD_PI",
19242
+ "PI_CREW_ALLOW_MOCK"
19243
+ ];
19142
19244
  ChildPiLineObserver = class {
19143
19245
  buffer = "";
19144
19246
  input;
@@ -46743,7 +46845,7 @@ function buildTeamDoctorReport(input) {
46743
46845
  );
46744
46846
  const sections = [
46745
46847
  section("Runtime", () => {
46746
- const git4 = commandExists("git", ["--version"]);
46848
+ const git3 = commandExists("git", ["--version"]);
46747
46849
  const pi = piCommandExists();
46748
46850
  return [
46749
46851
  { label: "cwd", ok: true, detail: input.cwd },
@@ -46753,7 +46855,7 @@ function buildTeamDoctorReport(input) {
46753
46855
  detail: `${process.platform}/${process.arch} node=${process.version}`
46754
46856
  },
46755
46857
  { label: "pi command", ok: pi.ok, detail: pi.detail },
46756
- { label: "git command", ok: git4.ok, detail: git4.detail },
46858
+ { label: "git command", ok: git3.ok, detail: git3.detail },
46757
46859
  {
46758
46860
  label: "config",
46759
46861
  ok: input.configErrors.length === 0,
@@ -53491,9 +53593,41 @@ var init_goal_achievement = __esm({
53491
53593
  }
53492
53594
  });
53493
53595
 
53596
+ // src/utils/token-counter.ts
53597
+ function isWhitespace(c) {
53598
+ return c === 32 || c === 9 || c === 10 || c === 13;
53599
+ }
53600
+ function isAlphanumeric(c) {
53601
+ return c >= 48 && c <= 57 || // 0-9
53602
+ c >= 65 && c <= 90 || // A-Z
53603
+ c >= 97 && c <= 122 || // a-z
53604
+ c === 95;
53605
+ }
53606
+ function countTokens(text) {
53607
+ if (!text || text.length === 0) return 0;
53608
+ let alpha = 0;
53609
+ let punct = 0;
53610
+ const len = text.length;
53611
+ for (let i2 = 0; i2 < len; i2++) {
53612
+ const c = text.charCodeAt(i2);
53613
+ if (isWhitespace(c)) continue;
53614
+ if (isAlphanumeric(c)) {
53615
+ alpha++;
53616
+ } else {
53617
+ punct++;
53618
+ }
53619
+ }
53620
+ return Math.ceil(alpha / 4) + punct;
53621
+ }
53622
+ var init_token_counter = __esm({
53623
+ "src/utils/token-counter.ts"() {
53624
+ "use strict";
53625
+ }
53626
+ });
53627
+
53494
53628
  // src/runtime/tool-output-pruner.ts
53495
53629
  function estimateTokens(text) {
53496
- return Math.ceil(text.length / 4);
53630
+ return countTokens(text);
53497
53631
  }
53498
53632
  function firstErrorLine(text) {
53499
53633
  return text.split(/\r?\n/).find((line4) => /error|failed|exception|panic/i.test(line4))?.trim();
@@ -53524,7 +53658,7 @@ function createPrunedNotice(tokens, entry) {
53524
53658
  const generic = `[Output pruned \u2014 ${tokens} tokens]`;
53525
53659
  const digest = resultDigest(entry.toolName, entry.content, entry.isError);
53526
53660
  if (!digest) return generic;
53527
- const genericTokens = Math.ceil(generic.length / 4);
53661
+ const genericTokens = countTokens(generic);
53528
53662
  const maxTokens = Math.max(genericTokens, Math.floor(genericTokens * DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER));
53529
53663
  const prefix = `[Output pruned \u2014 ${tokens} tokens; `;
53530
53664
  const suffix = "]";
@@ -53599,7 +53733,7 @@ function pruneToolOutputs(results, config = DEFAULT_PRUNE_CONFIG) {
53599
53733
  entry,
53600
53734
  tokens,
53601
53735
  notice,
53602
- savings: Math.max(0, tokens - Math.ceil(notice.length / 4))
53736
+ savings: Math.max(0, tokens - countTokens(notice))
53603
53737
  });
53604
53738
  accumulatedTokens += tokens;
53605
53739
  }
@@ -53624,6 +53758,7 @@ var DEFAULT_PRUNE_CONFIG, DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER, READ_SELECTOR_SUFF
53624
53758
  var init_tool_output_pruner = __esm({
53625
53759
  "src/runtime/tool-output-pruner.ts"() {
53626
53760
  "use strict";
53761
+ init_token_counter();
53627
53762
  DEFAULT_PRUNE_CONFIG = {
53628
53763
  protectTokens: 4e4,
53629
53764
  minimumSavings: 2e4,
@@ -55008,60 +55143,70 @@ var init_task_graph_scheduler = __esm({
55008
55143
  });
55009
55144
 
55010
55145
  // src/worktree/worktree-manager.ts
55011
- import { execFileSync as execFileSync6, spawnSync as spawnSync3 } from "node:child_process";
55146
+ import { execFile, execFileSync as execFileSync6, spawnSync as spawnSync3 } from "node:child_process";
55012
55147
  import { randomBytes as randomBytes2 } from "node:crypto";
55013
55148
  import * as fs75 from "node:fs";
55014
55149
  import * as path63 from "node:path";
55015
- function git3(cwd, args) {
55016
- return execFileSync6("git", args, {
55150
+ import { promisify } from "node:util";
55151
+ function gitEnv() {
55152
+ return {
55153
+ ...sanitizeEnvSecrets(process.env, {
55154
+ allowList: [
55155
+ "PATH",
55156
+ "HOME",
55157
+ "USER",
55158
+ ...WINDOWS_ESSENTIAL_ENV_VARS,
55159
+ "SHELL",
55160
+ "TERM",
55161
+ "LANG",
55162
+ "LC_ALL",
55163
+ "LC_COLLATE",
55164
+ "LC_CTYPE",
55165
+ "LC_MESSAGES",
55166
+ "XDG_CONFIG_HOME",
55167
+ "XDG_DATA_HOME",
55168
+ "XDG_CACHE_HOME",
55169
+ "NVM_BIN",
55170
+ "NVM_DIR",
55171
+ "NODE_PATH",
55172
+ "GIT_CONFIG_GLOBAL",
55173
+ "GIT_CONFIG_SYSTEM",
55174
+ "GIT_AUTHOR_NAME",
55175
+ "GIT_AUTHOR_EMAIL",
55176
+ "GIT_COMMITTER_NAME",
55177
+ "GIT_COMMITTER_EMAIL"
55178
+ ]
55179
+ }),
55180
+ LANG: "en_US.UTF-8",
55181
+ LC_ALL: "en_US.UTF-8"
55182
+ };
55183
+ }
55184
+ async function gitAsync(cwd, args) {
55185
+ const { stdout } = await execFileAsync("git", args, {
55017
55186
  cwd,
55018
55187
  encoding: "utf-8",
55019
- stdio: ["ignore", "pipe", "pipe"],
55020
- env: {
55021
- ...sanitizeEnvSecrets(process.env, {
55022
- allowList: [
55023
- "PATH",
55024
- "HOME",
55025
- "USER",
55026
- ...WINDOWS_ESSENTIAL_ENV_VARS,
55027
- "SHELL",
55028
- "TERM",
55029
- "LANG",
55030
- "LC_ALL",
55031
- "LC_COLLATE",
55032
- "LC_CTYPE",
55033
- "LC_MESSAGES",
55034
- "XDG_CONFIG_HOME",
55035
- "XDG_DATA_HOME",
55036
- "XDG_CACHE_HOME",
55037
- "NVM_BIN",
55038
- "NVM_DIR",
55039
- "NODE_PATH",
55040
- "GIT_CONFIG_GLOBAL",
55041
- "GIT_CONFIG_SYSTEM",
55042
- "GIT_AUTHOR_NAME",
55043
- "GIT_AUTHOR_EMAIL",
55044
- "GIT_COMMITTER_NAME",
55045
- "GIT_COMMITTER_EMAIL"
55046
- ]
55047
- }),
55048
- LANG: "en_US.UTF-8",
55049
- LC_ALL: "en_US.UTF-8"
55050
- },
55188
+ env: gitEnv(),
55051
55189
  windowsHide: true
55052
- }).trim();
55190
+ });
55191
+ return stdout.trim();
55053
55192
  }
55054
55193
  function sanitizeBranchPart2(value) {
55055
55194
  return value.toLowerCase().replace(/[^a-z0-9_/-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
55056
55195
  }
55057
- function findGitRoot(cwd) {
55058
- return git3(cwd, ["rev-parse", "--show-toplevel"]);
55196
+ async function findGitRootAsync(cwd) {
55197
+ const cached = _gitRootCache.get(cwd);
55198
+ if (cached) return cached;
55199
+ const root = await gitAsync(cwd, ["rev-parse", "--show-toplevel"]);
55200
+ _gitRootCache.set(cwd, root);
55201
+ return root;
55059
55202
  }
55060
- function assertCleanLeader(repoRoot) {
55061
- const status = git3(repoRoot, ["status", "--porcelain"]);
55203
+ async function assertCleanLeaderAsync(repoRoot) {
55204
+ if (_cleanLeaderCache.has(repoRoot)) return;
55205
+ const status = await gitAsync(repoRoot, ["status", "--porcelain"]);
55062
55206
  if (status.trim()) {
55063
55207
  throw new Error("Worktree mode requires a clean leader repository. Commit/stash changes or use workspaceMode: 'single'.");
55064
55208
  }
55209
+ _cleanLeaderCache.add(repoRoot);
55065
55210
  }
55066
55211
  function linkNodeModulesIfPresent(repoRoot, worktreePath) {
55067
55212
  const source = path63.join(repoRoot, "node_modules");
@@ -55233,31 +55378,30 @@ function runSetupHook(manifest, task, repoRoot, worktreePath, branch) {
55233
55378
  return [];
55234
55379
  }
55235
55380
  }
55236
- function branchExists(repoRoot, branch) {
55381
+ async function branchExistsAsync(repoRoot, branch) {
55237
55382
  let local = false;
55238
55383
  try {
55239
- git3(repoRoot, ["rev-parse", "--verify", `refs/heads/${branch}`]);
55384
+ await gitAsync(repoRoot, ["rev-parse", "--verify", `refs/heads/${branch}`]);
55240
55385
  local = true;
55241
55386
  } catch {
55242
55387
  }
55243
55388
  if (local) return { local: true, remoteOnly: false };
55244
55389
  try {
55245
- const out = execFileSync6("git", ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`], {
55390
+ const out = (await execFileAsync("git", ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`], {
55246
55391
  cwd: repoRoot,
55247
55392
  encoding: "utf-8",
55248
- stdio: ["ignore", "pipe", "pipe"],
55249
55393
  windowsHide: true
55250
- }).trim();
55394
+ })).stdout.trim();
55251
55395
  return { local: false, remoteOnly: out.length > 0 };
55252
55396
  } catch {
55253
55397
  return { local: false, remoteOnly: false };
55254
55398
  }
55255
55399
  }
55256
- function pruneStaleWorktrees(repoRoot) {
55400
+ async function pruneStaleWorktreesAsync(repoRoot) {
55257
55401
  try {
55258
- execFileSync6("git", ["worktree", "prune"], {
55402
+ await execFileAsync("git", ["worktree", "prune"], {
55259
55403
  cwd: repoRoot,
55260
- stdio: "ignore"
55404
+ windowsHide: true
55261
55405
  });
55262
55406
  } catch {
55263
55407
  }
@@ -55322,11 +55466,11 @@ function overlaySeedPaths(repoRoot, worktreePath, seedPaths) {
55322
55466
  });
55323
55467
  }
55324
55468
  }
55325
- function prepareTaskWorkspace(manifest, task, stepSeedPaths) {
55469
+ async function prepareTaskWorkspaceAsync(manifest, task, stepSeedPaths) {
55326
55470
  if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
55327
- const repoRoot = findGitRoot(manifest.cwd);
55471
+ const repoRoot = await findGitRootAsync(manifest.cwd);
55328
55472
  const loadedConfig = loadConfig(manifest.cwd);
55329
- if (loadedConfig.config.requireCleanWorktreeLeader !== false) assertCleanLeader(repoRoot);
55473
+ if (loadedConfig.config.requireCleanWorktreeLeader !== false) await assertCleanLeaderAsync(repoRoot);
55330
55474
  const sanitizedRunId = manifest.runId.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "run";
55331
55475
  const worktreeRoot = path63.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, sanitizedRunId);
55332
55476
  fs75.mkdirSync(worktreeRoot, { recursive: true });
@@ -55345,7 +55489,7 @@ function prepareTaskWorkspace(manifest, task, stepSeedPaths) {
55345
55489
  const branch = `pi-crew/${sanitizeBranchPart2(manifest.runId)}/${sanitizeBranchPart2(task.id)}`;
55346
55490
  let worktreeExists = false;
55347
55491
  try {
55348
- const worktreeList = git3(repoRoot, ["worktree", "list", "--porcelain"]);
55492
+ const worktreeList = await gitAsync(repoRoot, ["worktree", "list", "--porcelain"]);
55349
55493
  const normalizedWtPath = process.platform === "win32" ? (() => {
55350
55494
  try {
55351
55495
  const r = fs75.realpathSync.native(worktreePath);
@@ -55368,7 +55512,7 @@ function prepareTaskWorkspace(manifest, task, stepSeedPaths) {
55368
55512
  if (worktreeExists) {
55369
55513
  let currentBranch;
55370
55514
  try {
55371
- currentBranch = git3(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
55515
+ currentBranch = await gitAsync(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
55372
55516
  } catch (gitError) {
55373
55517
  throw new Error(
55374
55518
  `Existing worktree at ${worktreePath} is not a valid git repository; cannot verify branch: ${gitError instanceof Error ? gitError.message : String(gitError)}`
@@ -55377,30 +55521,31 @@ function prepareTaskWorkspace(manifest, task, stepSeedPaths) {
55377
55521
  if (currentBranch !== branch) {
55378
55522
  throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
55379
55523
  }
55380
- const dirtyStatus = git3(worktreePath, ["status", "--porcelain"]);
55524
+ const dirtyStatus = await gitAsync(worktreePath, ["status", "--porcelain"]);
55381
55525
  if (dirtyStatus.trim()) {
55382
55526
  logInternalError(
55383
55527
  "worktree.reused.dirty",
55384
55528
  new Error(`Discarding uncommitted changes in reused worktree at ${worktreePath}`),
55385
55529
  `runId=${manifest.runId}, taskId=${task.id}, dirtyStatus=${dirtyStatus.trim()}`
55386
55530
  );
55387
- git3(worktreePath, ["checkout", "--", "."]);
55388
- git3(worktreePath, ["clean", "-fd"]);
55531
+ await gitAsync(worktreePath, ["checkout", "--", "."]);
55532
+ await gitAsync(worktreePath, ["clean", "-fd"]);
55389
55533
  }
55390
55534
  const globalSeedPaths2 = loadedConfig.config.worktree?.seedPaths ?? [];
55391
55535
  const mergedReused = normalizeSeedPaths([...globalSeedPaths2, ...stepSeedPaths ?? []], repoRoot);
55392
55536
  if (mergedReused.length > 0) {
55393
55537
  overlaySeedPaths(repoRoot, worktreePath, mergedReused);
55394
55538
  }
55395
- assertCleanLeader(repoRoot);
55539
+ _cleanLeaderCache.delete(repoRoot);
55540
+ await assertCleanLeaderAsync(repoRoot);
55396
55541
  return { cwd: worktreePath, worktreePath, branch, reused: true };
55397
55542
  }
55398
- pruneStaleWorktrees(repoRoot);
55399
- const exists = branchExists(repoRoot, branch);
55543
+ await pruneStaleWorktreesAsync(repoRoot);
55544
+ const exists = await branchExistsAsync(repoRoot, branch);
55400
55545
  let worktreeCreated = false;
55401
55546
  try {
55402
55547
  if (exists.local) {
55403
- git3(repoRoot, ["worktree", "add", worktreePath, branch]);
55548
+ await gitAsync(repoRoot, ["worktree", "add", worktreePath, branch]);
55404
55549
  } else {
55405
55550
  if (exists.remoteOnly) {
55406
55551
  logInternalError(
@@ -55409,7 +55554,7 @@ function prepareTaskWorkspace(manifest, task, stepSeedPaths) {
55409
55554
  `branch=${branch}`
55410
55555
  );
55411
55556
  }
55412
- git3(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
55557
+ await gitAsync(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
55413
55558
  }
55414
55559
  worktreeCreated = true;
55415
55560
  } catch (error) {
@@ -55443,10 +55588,10 @@ function prepareTaskWorkspace(manifest, task, stepSeedPaths) {
55443
55588
  syntheticPaths
55444
55589
  };
55445
55590
  }
55446
- function captureWorktreeDiffStat(worktreePath) {
55591
+ async function captureWorktreeDiffStatAsync(worktreePath) {
55447
55592
  try {
55448
- const diffStat = git3(worktreePath, ["diff", "--stat"]);
55449
- const numstat = git3(worktreePath, ["diff", "--numstat"]);
55593
+ const diffStat = await gitAsync(worktreePath, ["diff", "--stat"]);
55594
+ const numstat = await gitAsync(worktreePath, ["diff", "--numstat"]);
55450
55595
  let filesChanged = 0;
55451
55596
  let insertions = 0;
55452
55597
  let deletions = 0;
@@ -55461,36 +55606,37 @@ function captureWorktreeDiffStat(worktreePath) {
55461
55606
  return { filesChanged: 0, insertions: 0, deletions: 0, diffStat: "" };
55462
55607
  }
55463
55608
  }
55464
- function captureWorktreeDiff(worktreePath) {
55609
+ async function captureWorktreeDiffAsync(worktreePath) {
55465
55610
  try {
55466
- return git3(worktreePath, ["diff", "--stat"]) + "\n\n" + git3(worktreePath, ["diff"]);
55611
+ const [stat2, full] = await Promise.all([gitAsync(worktreePath, ["diff", "--stat"]), gitAsync(worktreePath, ["diff"])]);
55612
+ return stat2 + "\n\n" + full;
55467
55613
  } catch (error) {
55468
55614
  const message = error instanceof Error ? error.message : String(error);
55469
55615
  return `Failed to capture worktree diff: ${message}`;
55470
55616
  }
55471
55617
  }
55472
- function prepareAgentWorktree(manifest, agentId) {
55618
+ async function prepareAgentWorktreeAsync(manifest, agentId) {
55473
55619
  try {
55474
- const repoRoot = findGitRoot(manifest.cwd);
55620
+ const repoRoot = await findGitRootAsync(manifest.cwd);
55475
55621
  const loadedConfig = loadConfig(manifest.cwd);
55476
- if (loadedConfig.config.requireCleanWorktreeLeader !== false) assertCleanLeader(repoRoot);
55622
+ if (loadedConfig.config.requireCleanWorktreeLeader !== false) await assertCleanLeaderAsync(repoRoot);
55477
55623
  const sanitizedRunId = manifest.runId.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "run";
55478
55624
  const worktreeRoot = path63.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, sanitizedRunId);
55479
55625
  fs75.mkdirSync(worktreeRoot, { recursive: true });
55480
55626
  const sanitizedAgentId = sanitizeBranchPart2(agentId);
55481
55627
  const worktreePath = path63.join(worktreeRoot, sanitizedAgentId);
55482
55628
  const branch = `pi-crew/${sanitizedRunId}/${sanitizedAgentId}`;
55483
- pruneStaleWorktrees(repoRoot);
55484
- git3(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
55629
+ await pruneStaleWorktreesAsync(repoRoot);
55630
+ await gitAsync(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
55485
55631
  const nodeModulesLinked = loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
55486
55632
  return { cwd: worktreePath, worktreePath, branch, nodeModulesLinked };
55487
55633
  } catch {
55488
55634
  return void 0;
55489
55635
  }
55490
55636
  }
55491
- function cleanupAgentWorktree(manifest, worktreePath, branch) {
55637
+ async function cleanupAgentWorktreeAsync(manifest, worktreePath, branch) {
55492
55638
  try {
55493
- const diff = captureWorktreeDiff(worktreePath);
55639
+ const diff = await captureWorktreeDiffAsync(worktreePath);
55494
55640
  if (diff.trim() && !diff.startsWith("Failed to capture worktree diff")) {
55495
55641
  writeArtifact(manifest.artifactsRoot, {
55496
55642
  kind: "diff",
@@ -55502,9 +55648,19 @@ function cleanupAgentWorktree(manifest, worktreePath, branch) {
55502
55648
  } catch (error) {
55503
55649
  logInternalError("worktree.agent-cleanup.diff", error, `worktreePath=${worktreePath}`);
55504
55650
  }
55651
+ let repoRoot;
55652
+ try {
55653
+ repoRoot = await findGitRootAsync(manifest.cwd);
55654
+ } catch {
55655
+ try {
55656
+ fs75.rmSync(worktreePath, { recursive: true, force: true });
55657
+ } catch (rmError) {
55658
+ logInternalError("worktree.agent-cleanup.rm", rmError, `worktreePath=${worktreePath}`);
55659
+ }
55660
+ return;
55661
+ }
55505
55662
  try {
55506
- const repoRoot = findGitRoot(manifest.cwd);
55507
- git3(repoRoot, ["worktree", "remove", "--force", worktreePath]);
55663
+ await gitAsync(repoRoot, ["worktree", "remove", "--force", worktreePath]);
55508
55664
  } catch (error) {
55509
55665
  logInternalError("worktree.agent-cleanup.remove", error, `worktreePath=${worktreePath}`);
55510
55666
  try {
@@ -55515,19 +55671,18 @@ function cleanupAgentWorktree(manifest, worktreePath, branch) {
55515
55671
  }
55516
55672
  if (branch) {
55517
55673
  try {
55518
- const repoRoot = findGitRoot(manifest.cwd);
55519
- git3(repoRoot, ["branch", "-D", branch]);
55674
+ await gitAsync(repoRoot, ["branch", "-D", branch]);
55520
55675
  } catch (error) {
55521
55676
  logInternalError("worktree.agent-cleanup.branch", error, `branch=${branch}`);
55522
55677
  }
55523
55678
  }
55524
55679
  try {
55525
- const repoRoot = findGitRoot(manifest.cwd);
55526
- git3(repoRoot, ["worktree", "prune"]);
55680
+ await gitAsync(repoRoot, ["worktree", "prune"]);
55527
55681
  } catch (error) {
55528
55682
  logInternalError("worktree.agent-cleanup.prune", error, `worktreePath=${worktreePath}`);
55529
55683
  }
55530
55684
  }
55685
+ var execFileAsync, _gitRootCache, _cleanLeaderCache;
55531
55686
  var init_worktree_manager = __esm({
55532
55687
  "src/worktree/worktree-manager.ts"() {
55533
55688
  "use strict";
@@ -55538,6 +55693,9 @@ var init_worktree_manager = __esm({
55538
55693
  init_env_filter();
55539
55694
  init_internal_error();
55540
55695
  init_paths();
55696
+ execFileAsync = promisify(execFile);
55697
+ _gitRootCache = /* @__PURE__ */ new Map();
55698
+ _cleanLeaderCache = /* @__PURE__ */ new Set();
55541
55699
  }
55542
55700
  });
55543
55701
 
@@ -57496,7 +57654,7 @@ async function runTeamTask(input) {
57496
57654
  let streamBridge;
57497
57655
  try {
57498
57656
  streamBridge = registerStreamBridge(manifest.runId);
57499
- const workspace = prepareTaskWorkspace(manifest, input.task, input.step.seedPaths);
57657
+ const workspace = await prepareTaskWorkspaceAsync(manifest, input.task, input.step.seedPaths);
57500
57658
  const worktree = workspace.worktreePath && workspace.branch ? {
57501
57659
  path: workspace.worktreePath,
57502
57660
  branch: workspace.branch,
@@ -58154,13 +58312,13 @@ async function runTeamTask(input) {
58154
58312
  const diffArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
58155
58313
  kind: "diff",
58156
58314
  relativePath: `diffs/${task.id}.diff`,
58157
- content: captureWorktreeDiff(workspace.worktreePath),
58315
+ content: await captureWorktreeDiffAsync(workspace.worktreePath),
58158
58316
  producer: task.id
58159
58317
  }) : void 0;
58160
58318
  const diffStatArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
58161
58319
  kind: "metadata",
58162
58320
  relativePath: `metadata/${task.id}.diff-stat.json`,
58163
- content: `${JSON.stringify({ ...captureWorktreeDiffStat(workspace.worktreePath), syntheticPaths: workspace.syntheticPaths ?? [], nodeModulesLinked: workspace.nodeModulesLinked ?? false }, null, 2)}
58321
+ content: `${JSON.stringify({ ...await captureWorktreeDiffStatAsync(workspace.worktreePath), syntheticPaths: workspace.syntheticPaths ?? [], nodeModulesLinked: workspace.nodeModulesLinked ?? false }, null, 2)}
58164
58322
  `,
58165
58323
  producer: task.id
58166
58324
  }) : void 0;
@@ -59399,6 +59557,7 @@ __export(team_runner_exports, {
59399
59557
  __test__mergeTaskUpdates: () => __test__mergeTaskUpdates,
59400
59558
  __test__parseAdaptivePlan: () => __test__parseAdaptivePlan,
59401
59559
  __test__repairAdaptivePlan: () => __test__repairAdaptivePlan,
59560
+ checkPerTaskBudget: () => checkPerTaskBudget,
59402
59561
  executeTeamRun: () => executeTeamRun,
59403
59562
  hasPendingMutatingTaskAtBoundary: () => hasPendingMutatingTaskAtBoundary,
59404
59563
  mergeTaskUpdatesPreservingTerminal: () => mergeTaskUpdatesPreservingTerminal,
@@ -59429,6 +59588,23 @@ function startTeamRunHeartbeat(stateRoot, runId) {
59429
59588
  const interval = setInterval(writeHeartbeat, 3e4);
59430
59589
  return () => clearInterval(interval);
59431
59590
  }
59591
+ function checkPerTaskBudget(tasks, budgetTotal, budgetWarning, budgetAbort, fairShareFraction = 0.5) {
59592
+ const usage = aggregateUsage(tasks);
59593
+ const totalUsed = (usage?.input ?? 0) + (usage?.output ?? 0) + (usage?.cacheWrite ?? 0);
59594
+ const abort = totalUsed >= budgetAbort * budgetTotal;
59595
+ const warning = !abort && totalUsed >= budgetWarning * budgetTotal;
59596
+ const remainingBudget = Math.max(0, budgetTotal - totalUsed);
59597
+ const fairShareThreshold = remainingBudget * fairShareFraction;
59598
+ const fairShareViolators = [];
59599
+ for (const task of tasks) {
59600
+ if (!task.usage) continue;
59601
+ const taskTotal = (task.usage.input ?? 0) + (task.usage.output ?? 0) + (task.usage.cacheWrite ?? 0);
59602
+ if (fairShareThreshold > 0 && taskTotal > fairShareThreshold && taskTotal > budgetTotal * 0.1) {
59603
+ fairShareViolators.push(task.id);
59604
+ }
59605
+ }
59606
+ return { abort, warning, fairShareViolators, totalUsed };
59607
+ }
59432
59608
  function findStep(workflow, task) {
59433
59609
  const step = workflow.steps.find((candidate) => candidate.id === task.stepId);
59434
59610
  if (!step) throw new Error(`Workflow step '${task.stepId}' not found for task '${task.id}'.`);
@@ -59737,6 +59913,13 @@ async function executeTeamRun(input) {
59737
59913
  "running",
59738
59914
  input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results."
59739
59915
  );
59916
+ if (input.budgetTotal !== void 0) manifest.budgetTotal = input.budgetTotal;
59917
+ if (input.budgetWarning !== void 0) manifest.budgetWarning = input.budgetWarning;
59918
+ if (input.budgetAbort !== void 0) manifest.budgetAbort = input.budgetAbort;
59919
+ if (input.budgetUnlimited !== void 0) manifest.budgetUnlimited = input.budgetUnlimited;
59920
+ if (manifest.budgetTotal !== void 0 && manifest.budgetTotal > 0 && !manifest.budgetUnlimited) {
59921
+ saveRunManifest(manifest);
59922
+ }
59740
59923
  void registerRunPromise(manifest.runId);
59741
59924
  const stopTeamHeartbeat = startTeamRunHeartbeat(manifest.stateRoot, manifest.runId);
59742
59925
  const cleanupUsage = () => {
@@ -60434,6 +60617,60 @@ async function executeTeamRunCore(input, manifest, workflow) {
60434
60617
  }
60435
60618
  wfMachine = { ...wfMachine, currentPhaseIndex: pi + 1 };
60436
60619
  }
60620
+ if (input.budgetTotal !== void 0 && input.budgetTotal > 0 && input.budgetUnlimited !== true) {
60621
+ const warnThreshold = input.budgetWarning ?? 0.8;
60622
+ const abortThreshold = input.budgetAbort ?? 0.95;
60623
+ const budgetCheck = checkPerTaskBudget(tasks, input.budgetTotal, warnThreshold, abortThreshold);
60624
+ if (budgetCheck.abort) {
60625
+ const message = `Per-task budget abort threshold exceeded: ${formatTokens(budgetCheck.totalUsed)}/${formatTokens(input.budgetTotal)} (${Math.round(budgetCheck.totalUsed / input.budgetTotal * 100)}%)`;
60626
+ console.warn(`[team-runner] ${message}`);
60627
+ await appendEventAsync(manifest.eventsPath, {
60628
+ type: "run.budget_abort",
60629
+ runId: manifest.runId,
60630
+ message,
60631
+ data: {
60632
+ budgetTotal: input.budgetTotal,
60633
+ budgetUsed: budgetCheck.totalUsed,
60634
+ threshold: "abort"
60635
+ }
60636
+ });
60637
+ tasks = markBlocked(tasks, `Budget abort threshold exceeded: ${message}`);
60638
+ await saveRunTasksAsync(manifest, tasks);
60639
+ manifest = updateRunStatus(manifest, "failed", message);
60640
+ return { manifest, tasks };
60641
+ }
60642
+ if (budgetCheck.warning) {
60643
+ const message = `Per-task budget warning threshold crossed: ${formatTokens(budgetCheck.totalUsed)}/${formatTokens(input.budgetTotal)} (${Math.round(budgetCheck.totalUsed / input.budgetTotal * 100)}%)`;
60644
+ console.warn(`[team-runner] ${message}`);
60645
+ await appendEventAsync(manifest.eventsPath, {
60646
+ type: "run.budget_warning",
60647
+ runId: manifest.runId,
60648
+ message,
60649
+ data: {
60650
+ budgetTotal: input.budgetTotal,
60651
+ budgetUsed: budgetCheck.totalUsed,
60652
+ threshold: "warning"
60653
+ }
60654
+ });
60655
+ }
60656
+ for (const violatorId of budgetCheck.fairShareViolators) {
60657
+ const violator = tasks.find((t2) => t2.id === violatorId);
60658
+ if (!violator) continue;
60659
+ const taskTotal = (violator.usage?.input ?? 0) + (violator.usage?.output ?? 0) + (violator.usage?.cacheWrite ?? 0);
60660
+ const message = `Task '${violatorId}' consumed ${formatTokens(taskTotal)} (${Math.round(taskTotal / input.budgetTotal * 100)}% of total budget) \u2014 exceeds fair share`;
60661
+ console.warn(`[team-runner.fair-share] ${message}`);
60662
+ await appendEventAsync(manifest.eventsPath, {
60663
+ type: "task.budget_fair_share",
60664
+ runId: manifest.runId,
60665
+ taskId: violatorId,
60666
+ message,
60667
+ data: {
60668
+ budgetTotal: input.budgetTotal,
60669
+ taskUsage: taskTotal
60670
+ }
60671
+ });
60672
+ }
60673
+ }
60437
60674
  const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
60438
60675
  if (cancelledResult || input.signal?.aborted) {
60439
60676
  const reason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : void 0;
@@ -68838,7 +69075,7 @@ function makeWorkflowCtx(manifest, opts) {
68838
69075
  const task = composeAgentTask(call);
68839
69076
  let agentCwd = manifest.cwd;
68840
69077
  if (call.worktree === true) {
68841
- const wt = prepareAgentWorktree(manifest, `dwf-agent-${Date.now()}-${randomBytes3(4).toString("hex")}`);
69078
+ const wt = await prepareAgentWorktreeAsync(manifest, `dwf-agent-${Date.now()}-${randomBytes3(4).toString("hex")}`);
68842
69079
  if (wt?.worktreePath) {
68843
69080
  agentCwd = wt.cwd;
68844
69081
  worktreePath = wt.worktreePath;
@@ -68913,7 +69150,7 @@ function makeWorkflowCtx(manifest, opts) {
68913
69150
  } finally {
68914
69151
  if (worktreePath) {
68915
69152
  try {
68916
- cleanupAgentWorktree(manifest, worktreePath, worktreeBranch);
69153
+ await cleanupAgentWorktreeAsync(manifest, worktreePath, worktreeBranch);
68917
69154
  } catch (cleanupError) {
68918
69155
  logInternalError("dynamic-workflow-context.worktree-cleanup", cleanupError, `worktreePath=${worktreePath}`);
68919
69156
  }
@@ -69609,7 +69846,7 @@ async function handleRun(params, ctx) {
69609
69846
  let resolvedCtx = ctx;
69610
69847
  if (workingDir) {
69611
69848
  try {
69612
- const gitRoot = findGitRoot(workingDir);
69849
+ const gitRoot = await findGitRootAsync(workingDir);
69613
69850
  if (gitRoot && gitRoot !== workingDir) {
69614
69851
  resolvedCtx = { ...ctx, cwd: gitRoot };
69615
69852
  }
@@ -69619,7 +69856,7 @@ async function handleRun(params, ctx) {
69619
69856
  if (params.workspaceMode === "worktree") {
69620
69857
  let gitRoot;
69621
69858
  try {
69622
- gitRoot = findGitRoot(resolvedCtx.cwd);
69859
+ gitRoot = await findGitRootAsync(resolvedCtx.cwd);
69623
69860
  } catch {
69624
69861
  }
69625
69862
  if (!gitRoot) {
@@ -69633,7 +69870,7 @@ Use workspaceMode: 'single' or run from a git repository.`,
69633
69870
  const preCheckConfig = loadConfig(resolvedCtx.cwd);
69634
69871
  if (preCheckConfig.config.requireCleanWorktreeLeader !== false) {
69635
69872
  try {
69636
- assertCleanLeader(gitRoot);
69873
+ await assertCleanLeaderAsync(gitRoot);
69637
69874
  } catch (err2) {
69638
69875
  const msg = err2 instanceof Error ? err2.message : String(err2);
69639
69876
  return result(
@@ -69885,10 +70122,30 @@ ${dwfResult.manifest.summary ?? ""}`,
69885
70122
  const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
69886
70123
  const runtime = await resolveCrewRuntime(executedConfig);
69887
70124
  const runtimeResolution = runtimeResolutionState(runtime);
70125
+ if (process.env.PI_CREW_DEBUG_BUDGET === "1") {
70126
+ console.log(
70127
+ "[DEBUG budget] params keys:",
70128
+ Object.keys(params),
70129
+ "budgetTotal:",
70130
+ params.budgetTotal,
70131
+ "budgetWarning:",
70132
+ params.budgetWarning,
70133
+ "budgetAbort:",
70134
+ params.budgetAbort
70135
+ );
70136
+ }
69888
70137
  const executionManifest = {
69889
70138
  ...updatedManifest,
69890
70139
  runtimeResolution,
69891
70140
  runConfig: executedConfig,
70141
+ // Persist budget config on the manifest so it's observable post-run
70142
+ // (events.jsonl, status reads, audits). The team-runner reads these
70143
+ // from the input, but persisting them means consumers can verify
70144
+ // enforcement was armed without re-parsing the original call params.
70145
+ ...params.budgetTotal !== void 0 ? { budgetTotal: params.budgetTotal } : {},
70146
+ ...params.budgetWarning !== void 0 ? { budgetWarning: params.budgetWarning } : {},
70147
+ ...params.budgetAbort !== void 0 ? { budgetAbort: params.budgetAbort } : {},
70148
+ ...params.budgetUnlimited !== void 0 ? { budgetUnlimited: params.budgetUnlimited } : {},
69892
70149
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
69893
70150
  };
69894
70151
  atomicWriteJson(paths.manifestPath, executionManifest);
@@ -70153,7 +70410,11 @@ ${dwfResult.manifest.summary ?? ""}`,
70153
70410
  reliability: executedConfig.reliability,
70154
70411
  metricRegistry: ctx.metricRegistry,
70155
70412
  onJsonEvent: ctx.onJsonEvent,
70156
- workspaceId: ctx.sessionId ?? ctx.cwd
70413
+ workspaceId: ctx.sessionId ?? ctx.cwd,
70414
+ budgetTotal: params.budgetTotal,
70415
+ budgetWarning: params.budgetWarning,
70416
+ budgetAbort: params.budgetAbort,
70417
+ budgetUnlimited: params.budgetUnlimited
70157
70418
  });
70158
70419
  } finally {
70159
70420
  unregisterActiveRun(updatedManifest.runId);
@@ -70280,7 +70541,11 @@ ${dwfResult.manifest.summary ?? ""}`,
70280
70541
  reliability: executedConfig.reliability,
70281
70542
  metricRegistry: ctx.metricRegistry,
70282
70543
  onJsonEvent: ctx.onJsonEvent,
70283
- workspaceId: ctx.cwd
70544
+ workspaceId: ctx.cwd,
70545
+ budgetTotal: params.budgetTotal,
70546
+ budgetWarning: params.budgetWarning,
70547
+ budgetAbort: params.budgetAbort,
70548
+ budgetUnlimited: params.budgetUnlimited
70284
70549
  });
70285
70550
  } finally {
70286
70551
  unregisterActiveRun(updatedManifest.runId);
@@ -77264,7 +77529,7 @@ __export(otlp_exporter_exports, {
77264
77529
  convertToOTLP: () => convertToOTLP,
77265
77530
  validateEndpoint: () => validateEndpoint
77266
77531
  });
77267
- import { promisify } from "node:util";
77532
+ import { promisify as promisify2 } from "node:util";
77268
77533
  import { gzip } from "node:zlib";
77269
77534
  function validateEndpoint(endpoint) {
77270
77535
  let url;
@@ -77382,7 +77647,7 @@ var init_otlp_exporter = __esm({
77382
77647
  "use strict";
77383
77648
  init_internal_error();
77384
77649
  init_redaction();
77385
- gzipAsync = promisify(gzip);
77650
+ gzipAsync = promisify2(gzip);
77386
77651
  MAX_SNAPSHOTS_PER_PUSH = 5e3;
77387
77652
  OTLPExporter = class {
77388
77653
  name = "otlp";
@@ -81215,6 +81480,104 @@ var CREW_SHORTCUT_KEYS = CREW_SHORTCUTS.map((s) => s.key);
81215
81480
 
81216
81481
  // src/extension/cross-extension-rpc.ts
81217
81482
  init_safe_paths();
81483
+
81484
+ // src/extension/rpc-hmac.ts
81485
+ import { createHmac, randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
81486
+ var RPC_HMAC_VERSION = 1;
81487
+ var SECRET_ENV_VAR = "PI_CREW_RPC_SECRET";
81488
+ var CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1e3;
81489
+ var SIGNATURE_VALIDITY_MS = 10 * 60 * 1e3;
81490
+ function getRpcSecret() {
81491
+ return process.env[SECRET_ENV_VAR];
81492
+ }
81493
+ function isHmacEnabled() {
81494
+ return typeof process.env[SECRET_ENV_VAR] === "string" && process.env[SECRET_ENV_VAR].length > 0;
81495
+ }
81496
+ function verifyRpcSignature(payload, body) {
81497
+ const secret = getRpcSecret();
81498
+ if (!secret) {
81499
+ return { valid: false, reason: `[pi-crew HMAC] ${SECRET_ENV_VAR} not configured.` };
81500
+ }
81501
+ if (payload.version !== RPC_HMAC_VERSION) {
81502
+ return { valid: false, reason: `HMAC version mismatch: expected ${RPC_HMAC_VERSION}, got ${payload.version}` };
81503
+ }
81504
+ const now = Date.now();
81505
+ const age = now - payload.timestamp;
81506
+ if (age < -CLOCK_SKEW_TOLERANCE_MS) {
81507
+ return { valid: false, reason: `HMAC timestamp in the future by ${Math.abs(age)}ms` };
81508
+ }
81509
+ if (age > SIGNATURE_VALIDITY_MS) {
81510
+ return { valid: false, reason: `HMAC signature expired (${age}ms old)` };
81511
+ }
81512
+ const expected = computeHmac(secret, payload, body);
81513
+ return timingSafeCompare(payload.signature, expected) ? { valid: true } : { valid: false, reason: "HMAC signature mismatch" };
81514
+ }
81515
+ function withHmacVerification(handler, _channel) {
81516
+ return (params) => {
81517
+ if (!isHmacEnabled()) {
81518
+ return handler(params);
81519
+ }
81520
+ const sigPayload = extractSignaturePayload(params);
81521
+ if (!sigPayload) {
81522
+ throw new Error("[pi-crew HMAC] Missing HMAC signature in RPC request.");
81523
+ }
81524
+ const originalBody = stripHmacFields(params);
81525
+ const verification = verifyRpcSignature(sigPayload, originalBody);
81526
+ if (!verification.valid) {
81527
+ throw new Error(`[pi-crew HMAC] ${verification.reason}`);
81528
+ }
81529
+ return handler(originalBody);
81530
+ };
81531
+ }
81532
+ function stripHmacFields(params) {
81533
+ const result4 = { ...params };
81534
+ delete result4.hmacVersion;
81535
+ delete result4.hmacOrigin;
81536
+ delete result4.hmacTimestamp;
81537
+ delete result4.hmacChannel;
81538
+ delete result4.hmacNonce;
81539
+ delete result4.hmacSignature;
81540
+ return result4;
81541
+ }
81542
+ function computeHmac(secret, payload, body) {
81543
+ const payloadForHmac = {
81544
+ version: payload.version,
81545
+ origin: payload.origin,
81546
+ timestamp: payload.timestamp,
81547
+ channel: payload.channel,
81548
+ nonce: payload.nonce
81549
+ };
81550
+ const message = `${JSON.stringify(payloadForHmac)}:${JSON.stringify(body)}`;
81551
+ return createHmac("sha256", secret).update(message).digest("hex");
81552
+ }
81553
+ function timingSafeCompare(a, b) {
81554
+ const bufA = Buffer.from(a, "hex");
81555
+ const bufB = Buffer.from(b, "hex");
81556
+ const maxLen = Math.max(bufA.length, bufB.length);
81557
+ const safeA = Buffer.alloc(maxLen);
81558
+ const safeB = Buffer.alloc(maxLen);
81559
+ bufA.copy(safeA);
81560
+ bufB.copy(safeB);
81561
+ const equal = timingSafeEqual3(safeA, safeB);
81562
+ return equal && bufA.length === bufB.length;
81563
+ }
81564
+ function extractSignaturePayload(params) {
81565
+ if (!params || typeof params !== "object" || Array.isArray(params)) return null;
81566
+ const obj = params;
81567
+ if (typeof obj.hmacVersion !== "number" || typeof obj.hmacOrigin !== "string" || typeof obj.hmacTimestamp !== "number" || typeof obj.hmacChannel !== "string" || typeof obj.hmacNonce !== "string" || typeof obj.hmacSignature !== "string") {
81568
+ return null;
81569
+ }
81570
+ return {
81571
+ version: obj.hmacVersion,
81572
+ origin: obj.hmacOrigin,
81573
+ timestamp: obj.hmacTimestamp,
81574
+ channel: obj.hmacChannel,
81575
+ nonce: obj.hmacNonce,
81576
+ signature: obj.hmacSignature
81577
+ };
81578
+ }
81579
+
81580
+ // src/extension/cross-extension-rpc.ts
81218
81581
  init_live_control_realtime();
81219
81582
  var _cachedHandleTeamTool5;
81220
81583
  async function handleTeamTool6(params, ctx) {
@@ -81312,139 +81675,167 @@ function registerPiCrewRpc(events, getCtx) {
81312
81675
  on(
81313
81676
  events,
81314
81677
  "pi-crew:rpc:ping",
81315
- (raw) => reply(events, "pi-crew:rpc:ping", requestId2(raw), {
81316
- success: true,
81317
- data: { version: PI_CREW_RPC_VERSION }
81318
- })
81678
+ withHmacVerification(
81679
+ (raw) => reply(events, "pi-crew:rpc:ping", requestId2(raw), {
81680
+ success: true,
81681
+ data: { version: PI_CREW_RPC_VERSION }
81682
+ }),
81683
+ "pi-crew:rpc:ping"
81684
+ )
81319
81685
  ),
81320
- on(events, "pi-crew:rpc:run", async (raw) => {
81321
- const id = requestId2(raw);
81322
- try {
81323
- const rateLimit = checkRpcRateLimit();
81324
- if (!rateLimit.allowed) {
81686
+ on(
81687
+ events,
81688
+ "pi-crew:rpc:run",
81689
+ withHmacVerification(async (raw) => {
81690
+ const id = requestId2(raw);
81691
+ try {
81692
+ const rateLimit = checkRpcRateLimit();
81693
+ if (!rateLimit.allowed) {
81694
+ reply(events, "pi-crew:rpc:run", id, {
81695
+ success: false,
81696
+ error: `RPC run rate limit exceeded. Max ${RPC_RATE_LIMIT_MAX} requests per ${RPC_RATE_LIMIT_WINDOW_MS / 1e3}s. Retry after ${Math.ceil((rateLimit.retryAfterMs ?? 6e4) / 1e3)}s.`
81697
+ });
81698
+ return;
81699
+ }
81700
+ recordRpcRun();
81701
+ const ctx = getCtx();
81702
+ if (!ctx) throw new Error("No active pi-crew session context.");
81703
+ const ALLOWED_RPC_RUN_KEYS = /* @__PURE__ */ new Set([
81704
+ "goal",
81705
+ "team",
81706
+ "workflow",
81707
+ "async",
81708
+ "cwd",
81709
+ "config",
81710
+ "skill",
81711
+ "model",
81712
+ "budgetTotal",
81713
+ "budgetWarning",
81714
+ "budgetAbort",
81715
+ "budgetUnlimited"
81716
+ ]);
81717
+ let params;
81718
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
81719
+ const filtered = {
81720
+ ...raw
81721
+ };
81722
+ for (const key of Object.keys(filtered)) {
81723
+ if (!ALLOWED_RPC_RUN_KEYS.has(key)) delete filtered[key];
81724
+ }
81725
+ params = {
81726
+ ...filtered,
81727
+ action: "run"
81728
+ };
81729
+ } else {
81730
+ params = { action: "run" };
81731
+ }
81732
+ const permission = isAllowedRpcRunParams(params);
81733
+ if (!permission.ok) {
81734
+ reply(events, "pi-crew:rpc:run", id, {
81735
+ success: false,
81736
+ error: permission.error ?? "permission denied"
81737
+ });
81738
+ return;
81739
+ }
81740
+ const result4 = await handleTeamTool6(params, ctx);
81741
+ reply(
81742
+ events,
81743
+ "pi-crew:rpc:run",
81744
+ id,
81745
+ result4.isError ? { success: false, error: textOf(result4) } : { success: true, data: result4.details }
81746
+ );
81747
+ } catch (error) {
81325
81748
  reply(events, "pi-crew:rpc:run", id, {
81326
81749
  success: false,
81327
- error: `RPC run rate limit exceeded. Max ${RPC_RATE_LIMIT_MAX} requests per ${RPC_RATE_LIMIT_WINDOW_MS / 1e3}s. Retry after ${Math.ceil((rateLimit.retryAfterMs ?? 6e4) / 1e3)}s.`
81750
+ error: error instanceof Error ? error.message : String(error)
81328
81751
  });
81329
- return;
81330
- }
81331
- recordRpcRun();
81332
- const ctx = getCtx();
81333
- if (!ctx) throw new Error("No active pi-crew session context.");
81334
- const ALLOWED_RPC_RUN_KEYS = /* @__PURE__ */ new Set(["goal", "team", "workflow", "async", "cwd", "config", "skill", "model"]);
81335
- let params;
81336
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
81337
- const filtered = {
81338
- ...raw
81339
- };
81340
- for (const key of Object.keys(filtered)) {
81341
- if (!ALLOWED_RPC_RUN_KEYS.has(key)) delete filtered[key];
81342
- }
81343
- params = {
81344
- ...filtered,
81345
- action: "run"
81346
- };
81347
- } else {
81348
- params = { action: "run" };
81349
81752
  }
81350
- const permission = isAllowedRpcRunParams(params);
81351
- if (!permission.ok) {
81352
- reply(events, "pi-crew:rpc:run", id, {
81753
+ }, "pi-crew:rpc:run")
81754
+ ),
81755
+ on(
81756
+ events,
81757
+ "pi-crew:rpc:status",
81758
+ withHmacVerification(async (raw) => {
81759
+ const id = requestId2(raw);
81760
+ try {
81761
+ const ctx = getCtx();
81762
+ if (!ctx) throw new Error("No active pi-crew session context.");
81763
+ const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.runId : void 0;
81764
+ const result4 = await handleTeamTool6({ action: "status", runId }, ctx);
81765
+ reply(
81766
+ events,
81767
+ "pi-crew:rpc:status",
81768
+ id,
81769
+ result4.isError ? { success: false, error: textOf(result4) } : {
81770
+ success: true,
81771
+ data: {
81772
+ text: textOf(result4),
81773
+ details: result4.details
81774
+ }
81775
+ }
81776
+ );
81777
+ } catch (error) {
81778
+ reply(events, "pi-crew:rpc:status", id, {
81353
81779
  success: false,
81354
- error: permission.error ?? "permission denied"
81780
+ error: error instanceof Error ? error.message : String(error)
81355
81781
  });
81356
- return;
81357
81782
  }
81358
- const result4 = await handleTeamTool6(params, ctx);
81359
- reply(
81360
- events,
81361
- "pi-crew:rpc:run",
81362
- id,
81363
- result4.isError ? { success: false, error: textOf(result4) } : { success: true, data: result4.details }
81364
- );
81365
- } catch (error) {
81366
- reply(events, "pi-crew:rpc:run", id, {
81367
- success: false,
81368
- error: error instanceof Error ? error.message : String(error)
81369
- });
81370
- }
81371
- }),
81372
- on(events, "pi-crew:rpc:status", async (raw) => {
81373
- const id = requestId2(raw);
81374
- try {
81375
- const ctx = getCtx();
81376
- if (!ctx) throw new Error("No active pi-crew session context.");
81377
- const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.runId : void 0;
81378
- const result4 = await handleTeamTool6({ action: "status", runId }, ctx);
81379
- reply(
81380
- events,
81381
- "pi-crew:rpc:status",
81382
- id,
81383
- result4.isError ? { success: false, error: textOf(result4) } : {
81384
- success: true,
81385
- data: {
81386
- text: textOf(result4),
81387
- details: result4.details
81388
- }
81389
- }
81390
- );
81391
- } catch (error) {
81392
- reply(events, "pi-crew:rpc:status", id, {
81393
- success: false,
81394
- error: error instanceof Error ? error.message : String(error)
81395
- });
81396
- }
81397
- }),
81783
+ }, "pi-crew:rpc:status")
81784
+ ),
81398
81785
  on(events, "pi-crew:live-control", (raw) => {
81399
81786
  const request = parseLiveControlRealtimeMessage(raw);
81400
81787
  if (request) publishLiveControlRealtime(request);
81401
81788
  }),
81402
- on(events, "pi-crew:rpc:live-control", async (raw) => {
81403
- const id = requestId2(raw);
81404
- try {
81405
- const ctx = getCtx();
81406
- if (!ctx) throw new Error("No active pi-crew session context.");
81407
- const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
81408
- const rawOp = typeof obj.operation === "string" ? obj.operation : "steer-agent";
81409
- if (!isAllowedRpcOperation(rawOp)) {
81789
+ on(
81790
+ events,
81791
+ "pi-crew:rpc:live-control",
81792
+ withHmacVerification(async (raw) => {
81793
+ const id = requestId2(raw);
81794
+ try {
81795
+ const ctx = getCtx();
81796
+ if (!ctx) throw new Error("No active pi-crew session context.");
81797
+ const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
81798
+ const rawOp = typeof obj.operation === "string" ? obj.operation : "steer-agent";
81799
+ if (!isAllowedRpcOperation(rawOp)) {
81800
+ reply(events, "pi-crew:rpc:live-control", id, {
81801
+ success: false,
81802
+ error: `RPC operation '${rawOp}' is not allowed. Allowed: ${[...RPC_ALLOWED_OPERATIONS].join(", ")}`
81803
+ });
81804
+ return;
81805
+ }
81806
+ const result4 = await handleTeamTool6(
81807
+ {
81808
+ action: "api",
81809
+ runId: typeof obj.runId === "string" ? obj.runId : void 0,
81810
+ config: {
81811
+ operation: rawOp,
81812
+ agentId: obj.agentId,
81813
+ message: obj.message,
81814
+ prompt: obj.prompt
81815
+ }
81816
+ },
81817
+ ctx
81818
+ );
81819
+ reply(
81820
+ events,
81821
+ "pi-crew:rpc:live-control",
81822
+ id,
81823
+ result4.isError ? { success: false, error: textOf(result4) } : {
81824
+ success: true,
81825
+ data: {
81826
+ text: textOf(result4),
81827
+ details: result4.details
81828
+ }
81829
+ }
81830
+ );
81831
+ } catch (error) {
81410
81832
  reply(events, "pi-crew:rpc:live-control", id, {
81411
81833
  success: false,
81412
- error: `RPC operation '${rawOp}' is not allowed. Allowed: ${[...RPC_ALLOWED_OPERATIONS].join(", ")}`
81834
+ error: error instanceof Error ? error.message : String(error)
81413
81835
  });
81414
- return;
81415
81836
  }
81416
- const result4 = await handleTeamTool6(
81417
- {
81418
- action: "api",
81419
- runId: typeof obj.runId === "string" ? obj.runId : void 0,
81420
- config: {
81421
- operation: rawOp,
81422
- agentId: obj.agentId,
81423
- message: obj.message,
81424
- prompt: obj.prompt
81425
- }
81426
- },
81427
- ctx
81428
- );
81429
- reply(
81430
- events,
81431
- "pi-crew:rpc:live-control",
81432
- id,
81433
- result4.isError ? { success: false, error: textOf(result4) } : {
81434
- success: true,
81435
- data: {
81436
- text: textOf(result4),
81437
- details: result4.details
81438
- }
81439
- }
81440
- );
81441
- } catch (error) {
81442
- reply(events, "pi-crew:rpc:live-control", id, {
81443
- success: false,
81444
- error: error instanceof Error ? error.message : String(error)
81445
- });
81446
- }
81447
- })
81837
+ }, "pi-crew:rpc:live-control")
81838
+ )
81448
81839
  ];
81449
81840
  return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
81450
81841
  }