pi-crew 0.9.17 → 0.9.18

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
@@ -9058,7 +9058,7 @@ function flushOnePendingAtomicWrite(filePath) {
9058
9058
  pendingAtomicWrites.delete(filePath);
9059
9059
  throw error;
9060
9060
  }
9061
- const backoffMs = Math.min(3e4, current2.coalesceMs * Math.pow(2, current2.retryCount - 1));
9061
+ const backoffMs = Math.min(3e4, current2.coalesceMs * 2 ** (current2.retryCount - 1));
9062
9062
  const timer = setTimeout(() => flushOnePendingAtomicWrite(filePath), backoffMs);
9063
9063
  timer.unref();
9064
9064
  current2.timer = timer;
@@ -9701,9 +9701,14 @@ var init_suggestions = __esm({
9701
9701
  // src/config/config.ts
9702
9702
  var config_exports = {};
9703
9703
  __export(config_exports, {
9704
+ __test__configCacheSize: () => __test__configCacheSize,
9705
+ __test__getConfigCacheEntry: () => __test__getConfigCacheEntry,
9706
+ __test__getConfigCacheTtlMs: () => __test__getConfigCacheTtlMs,
9707
+ __test__setConfigCacheTtlMs: () => __test__setConfigCacheTtlMs,
9704
9708
  asRecord: () => asRecord,
9705
9709
  configPath: () => configPath,
9706
9710
  effectiveAutonomousConfig: () => effectiveAutonomousConfig,
9711
+ invalidateConfigCache: () => invalidateConfigCache,
9707
9712
  legacyConfigPath: () => legacyConfigPath,
9708
9713
  loadConfig: () => loadConfig,
9709
9714
  parseConfig: () => parseConfig,
@@ -9716,6 +9721,70 @@ __export(config_exports, {
9716
9721
  import * as fs5 from "node:fs";
9717
9722
  import * as os3 from "node:os";
9718
9723
  import * as path5 from "node:path";
9724
+ function __test__setConfigCacheTtlMs(ttlMs) {
9725
+ configCacheTtlMsOverride = ttlMs;
9726
+ }
9727
+ function __test__getConfigCacheTtlMs() {
9728
+ return configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
9729
+ }
9730
+ function __test__getConfigCacheEntry(parts) {
9731
+ return configCache.get(buildConfigCacheKey(parts));
9732
+ }
9733
+ function __test__configCacheSize() {
9734
+ return configCache.size;
9735
+ }
9736
+ function buildConfigCacheKey(parts) {
9737
+ return JSON.stringify([parts.filePath, parts.legacyPath, parts.projectPath, parts.projectPiCrewJsonPath, parts.cwd]);
9738
+ }
9739
+ function statMtimeMs(filePath) {
9740
+ try {
9741
+ return fs5.statSync(filePath).mtimeMs;
9742
+ } catch (error) {
9743
+ if (error.code === "ENOENT") return void 0;
9744
+ throw error;
9745
+ }
9746
+ }
9747
+ function readCachedConfigParts(filePath, legacyPath, cwd) {
9748
+ const projectPath2 = cwd ? projectConfigPath(cwd) : "";
9749
+ const piCrewJsonPath = cwd ? projectPiCrewJsonPath(cwd) : "";
9750
+ return {
9751
+ filePath,
9752
+ legacyPath,
9753
+ projectPath: projectPath2,
9754
+ projectPiCrewJsonPath: piCrewJsonPath,
9755
+ cwd: cwd ?? null
9756
+ };
9757
+ }
9758
+ function readCacheMtimes(parts) {
9759
+ const mtimes = {};
9760
+ for (const p of [parts.filePath, parts.legacyPath, parts.projectPath, parts.projectPiCrewJsonPath]) {
9761
+ if (!p) continue;
9762
+ const m = statMtimeMs(p);
9763
+ if (m !== void 0) mtimes[p] = m;
9764
+ }
9765
+ return mtimes;
9766
+ }
9767
+ function matchesCachedMtimes(cached, current2) {
9768
+ const cachedKeys = Object.keys(cached);
9769
+ const currentKeys = Object.keys(current2);
9770
+ if (cachedKeys.length !== currentKeys.length) return false;
9771
+ for (const key of cachedKeys) {
9772
+ if (current2[key] !== cached[key]) return false;
9773
+ }
9774
+ return true;
9775
+ }
9776
+ function setConfigCache(key, value, mtimes) {
9777
+ if (configCache.has(key)) configCache.delete(key);
9778
+ configCache.set(key, { value, mtimes, cachedAt: Date.now() });
9779
+ const ttlMs = configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
9780
+ const now = Date.now();
9781
+ for (const [k, entry] of configCache.entries()) {
9782
+ if (now - entry.cachedAt > ttlMs) configCache.delete(k);
9783
+ }
9784
+ }
9785
+ function invalidateConfigCache() {
9786
+ configCache.clear();
9787
+ }
9719
9788
  function resolveHomeDir() {
9720
9789
  const envValue = process.env.PI_TEAMS_HOME?.trim();
9721
9790
  const defaultHome = os3.homedir();
@@ -10448,6 +10517,18 @@ function readOptionalConfig(filePath) {
10448
10517
  function loadConfig(cwd) {
10449
10518
  const filePath = configPath();
10450
10519
  const legacyPath = legacyConfigPath();
10520
+ const cacheParts = readCachedConfigParts(filePath, legacyPath, cwd);
10521
+ const cacheKey2 = buildConfigCacheKey(cacheParts);
10522
+ const cached = configCache.get(cacheKey2);
10523
+ const ttlMs = configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
10524
+ if (cached && Date.now() - cached.cachedAt <= ttlMs) {
10525
+ const currentMtimes = readCacheMtimes(cacheParts);
10526
+ if (matchesCachedMtimes(cached.mtimes, currentMtimes)) {
10527
+ configCache.delete(cacheKey2);
10528
+ configCache.set(cacheKey2, cached);
10529
+ return cached.value;
10530
+ }
10531
+ }
10451
10532
  const paths = cwd ? [filePath, projectConfigPath(cwd)] : [filePath];
10452
10533
  const warnings = [];
10453
10534
  const legacyConfig = readOptionalConfig(legacyPath);
@@ -10480,12 +10561,16 @@ function loadConfig(cwd) {
10480
10561
  paths.push(piCrewJsonPath);
10481
10562
  }
10482
10563
  }
10483
- return {
10564
+ const result4 = {
10484
10565
  path: filePath,
10485
10566
  paths,
10486
10567
  config,
10487
10568
  warnings: warnings.length > 0 ? warnings : void 0
10488
10569
  };
10570
+ if (Object.keys(readCacheMtimes(cacheParts)).length > 0) {
10571
+ setConfigCache(cacheKey2, result4, readCacheMtimes(cacheParts));
10572
+ }
10573
+ return result4;
10489
10574
  }
10490
10575
  function updateConfig(patch, options = {}) {
10491
10576
  const filePath = options.scope === "project" && options.cwd ? projectConfigPath(options.cwd) : configPath();
@@ -10507,6 +10592,7 @@ function updateConfig(patch, options = {}) {
10507
10592
  fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
10508
10593
  atomicWriteFile(filePath, `${JSON.stringify(merged, null, 2)}
10509
10594
  `);
10595
+ invalidateConfigCache();
10510
10596
  return { path: filePath, config: merged };
10511
10597
  });
10512
10598
  }
@@ -10525,10 +10611,11 @@ function updateAutonomousConfig(patch) {
10525
10611
  current2.autonomous = { ...currentAutonomous, ...patch };
10526
10612
  atomicWriteFile(filePath, `${JSON.stringify(current2, null, 2)}
10527
10613
  `);
10614
+ invalidateConfigCache();
10528
10615
  return { path: filePath, config: parseConfig(current2) };
10529
10616
  });
10530
10617
  }
10531
- var KNOWN_TOP_LEVEL_KEYS, LIMIT_CEILINGS, DANGEROUS_OBJECT_KEYS;
10618
+ var CONFIG_CACHE_TTL_MS, configCache, configCacheTtlMsOverride, KNOWN_TOP_LEVEL_KEYS, LIMIT_CEILINGS, DANGEROUS_OBJECT_KEYS;
10532
10619
  var init_config = __esm({
10533
10620
  "src/config/config.ts"() {
10534
10621
  "use strict";
@@ -10540,6 +10627,9 @@ var init_config = __esm({
10540
10627
  init_internal_error();
10541
10628
  init_paths();
10542
10629
  init_suggestions();
10630
+ CONFIG_CACHE_TTL_MS = 2e3;
10631
+ configCache = /* @__PURE__ */ new Map();
10632
+ configCacheTtlMsOverride = null;
10543
10633
  KNOWN_TOP_LEVEL_KEYS = Object.keys(PiTeamsConfigSchema.properties ?? {});
10544
10634
  LIMIT_CEILINGS = {
10545
10635
  maxConcurrentWorkers: 1024,
@@ -12231,7 +12321,7 @@ function resolveCanonicalPath(p) {
12231
12321
  }
12232
12322
  function resolveWindowsCanonical(p) {
12233
12323
  try {
12234
- let real = fs9.realpathSync.native(p);
12324
+ const real = fs9.realpathSync.native(p);
12235
12325
  if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
12236
12326
  return real;
12237
12327
  } catch {
@@ -12331,7 +12421,6 @@ function resolveRealContainedPath(baseDir, targetPath2) {
12331
12421
  if (error.code === "EPERM" && process.platform === "win32") continue;
12332
12422
  if (error.code !== "ENOENT") throw error;
12333
12423
  if (i2 === resolvedParts.length - 1) continue;
12334
- continue;
12335
12424
  }
12336
12425
  }
12337
12426
  let targetFd;
@@ -12945,6 +13034,7 @@ __export(state_store_exports, {
12945
13034
  saveRunManifestAsync: () => saveRunManifestAsync,
12946
13035
  saveRunTasks: () => saveRunTasks,
12947
13036
  saveRunTasksAsync: () => saveRunTasksAsync,
13037
+ saveRunTasksCoalesced: () => saveRunTasksCoalesced,
12948
13038
  updateRunStatus: () => updateRunStatus
12949
13039
  });
12950
13040
  import * as fs12 from "node:fs";
@@ -13199,6 +13289,15 @@ function saveRunTasks(manifest, tasks) {
13199
13289
  tasksSize: tasksStat.size
13200
13290
  });
13201
13291
  }
13292
+ function saveRunTasksCoalesced(manifest, tasks) {
13293
+ invalidateRunCache(manifest.stateRoot);
13294
+ try {
13295
+ fs12.statSync(manifest.stateRoot);
13296
+ } catch {
13297
+ return;
13298
+ }
13299
+ atomicWriteJsonCoalesced(manifest.tasksPath, tasks);
13300
+ }
13202
13301
  async function saveRunTasksAsync(manifest, tasks) {
13203
13302
  invalidateRunCache(manifest.stateRoot);
13204
13303
  try {
@@ -18590,7 +18689,7 @@ ${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 1e-
18590
18689
  pendingOperations.clear();
18591
18690
  };
18592
18691
  let softLimitReached = false;
18593
- let steerInjectionFailed = false;
18692
+ const steerInjectionFailed = false;
18594
18693
  const maxTurns = input.maxTurns;
18595
18694
  let graceTurns = input.graceTurns;
18596
18695
  if (graceTurns !== void 0 && graceTurns > 1e3) graceTurns = 1e3;
@@ -44753,7 +44852,6 @@ function loadRunSummaries(cwd, options = {}) {
44753
44852
  // tasks stored separately, not in manifest
44754
44853
  });
44755
44854
  } catch {
44756
- continue;
44757
44855
  }
44758
44856
  }
44759
44857
  return summaries;
@@ -47299,7 +47397,6 @@ function snapshotManifests(cwd) {
47299
47397
  const content = fs59.readFileSync(abs);
47300
47398
  snapshot[rel] = createHash6("sha256").update(content).digest("hex");
47301
47399
  } catch {
47302
- continue;
47303
47400
  }
47304
47401
  }
47305
47402
  return snapshot;
@@ -47481,7 +47578,7 @@ function getBackgroundRunnerCommand(runnerPath, cwd, runId, loaderInput = resolv
47481
47578
  };
47482
47579
  }
47483
47580
  async function spawnBackgroundTeamRun(manifest) {
47484
- const runnerPath = path52.join(path52.dirname(fileURLToPath6(import.meta.url)), "background-runner.ts");
47581
+ const runnerPath = path52.join(packageRoot(), "src", "runtime", "background-runner.ts");
47485
47582
  const logPath = path52.join(manifest.stateRoot, "background.log");
47486
47583
  fs60.mkdirSync(manifest.stateRoot, { recursive: true });
47487
47584
  const filteredEnv = sanitizeEnvSecrets(process.env, {
@@ -47571,6 +47668,7 @@ var init_async_runner = __esm({
47571
47668
  init_env_allowlist();
47572
47669
  init_env_filter();
47573
47670
  init_internal_error();
47671
+ init_paths();
47574
47672
  init_orphan_worker_registry();
47575
47673
  init_peer_dep();
47576
47674
  requireFromHere = createRequire3(import.meta.url);
@@ -53140,18 +53238,6 @@ var init_branch_freshness = __esm({
53140
53238
  }
53141
53239
  });
53142
53240
 
53143
- // src/runtime/team-runner-artifacts.ts
53144
- function mergeArtifacts(items) {
53145
- const byPath = /* @__PURE__ */ new Map();
53146
- for (const item of items) byPath.set(item.path, item);
53147
- return [...byPath.values()];
53148
- }
53149
- var init_team_runner_artifacts = __esm({
53150
- "src/runtime/team-runner-artifacts.ts"() {
53151
- "use strict";
53152
- }
53153
- });
53154
-
53155
53241
  // src/runtime/coalesce-tasks.ts
53156
53242
  function planCoalescedGroups(readyTaskIds, tasks, workflow, enabled, maxGroupSize = DEFAULT_MAX_GROUP_SIZE, maxGroupBytes = DEFAULT_MAX_GROUP_BYTES) {
53157
53243
  if (!enabled || readyTaskIds.length === 0) return [];
@@ -53278,490 +53364,6 @@ var init_coalesce_tasks = __esm({
53278
53364
  }
53279
53365
  });
53280
53366
 
53281
- // src/runtime/task-runner/output-splitter.ts
53282
- function splitCoalescedOutput(rawOutput, taskIds) {
53283
- if (taskIds.length === 0) return [];
53284
- if (taskIds.length === 1) {
53285
- return [{ taskId: taskIds[0], text: rawOutput, strategy: "delimiter" }];
53286
- }
53287
- const byId = /* @__PURE__ */ new Map();
53288
- const delimiterHits = /* @__PURE__ */ new Set();
53289
- const delimiterRegex = /<<<TASK_RESULT:([^\s>]+)>>>([\s\S]*?)<<<END_TASK_RESULT>>>/g;
53290
- let match;
53291
- while ((match = delimiterRegex.exec(rawOutput)) !== null) {
53292
- const id = match[1];
53293
- const body = match[2].trim();
53294
- if (taskIds.includes(id) && !byId.has(id)) {
53295
- byId.set(id, body);
53296
- delimiterHits.add(id);
53297
- }
53298
- }
53299
- if (delimiterHits.size === taskIds.length) {
53300
- return taskIds.map((id) => ({
53301
- taskId: id,
53302
- text: byId.get(id) ?? "",
53303
- strategy: "delimiter"
53304
- }));
53305
- }
53306
- if (delimiterHits.size === 0) {
53307
- const bySection = parseBySectionHeadings(rawOutput, taskIds);
53308
- if (bySection.size === taskIds.length) {
53309
- return taskIds.map((id) => ({
53310
- taskId: id,
53311
- text: bySection.get(id) ?? "",
53312
- strategy: "section"
53313
- }));
53314
- }
53315
- }
53316
- return taskIds.map((id) => ({
53317
- taskId: id,
53318
- text: rawOutput,
53319
- strategy: "broadcast"
53320
- }));
53321
- }
53322
- function parseBySectionHeadings(rawOutput, taskIds) {
53323
- const result4 = /* @__PURE__ */ new Map();
53324
- const numberedRegex = /^(#{2,3})\s+Task\s+(\d+)\s+of\s+\d+.*$/gm;
53325
- const sections = [];
53326
- let m;
53327
- while ((m = numberedRegex.exec(rawOutput)) !== null) {
53328
- sections.push({ num: Number(m[2]), start: m.index + m[0].length });
53329
- }
53330
- if (sections.length >= taskIds.length) {
53331
- sections.sort((a, b) => a.start - b.start);
53332
- for (let i2 = 0; i2 < sections.length && i2 < taskIds.length; i2 += 1) {
53333
- const section2 = sections[i2];
53334
- const end = sections[i2 + 1]?.start ?? rawOutput.length;
53335
- const text = rawOutput.slice(section2.start, end).trim();
53336
- const taskId = taskIds[i2];
53337
- if (taskId) result4.set(taskId, text);
53338
- }
53339
- return result4;
53340
- }
53341
- const idHeaderRegex = /^(#{2,3})\s+Task\s+([^\s].*?)$/gm;
53342
- while ((m = idHeaderRegex.exec(rawOutput)) !== null) {
53343
- const headerText = m[2].trim();
53344
- const matchedId = taskIds.find((id) => headerText.includes(id));
53345
- if (matchedId && !result4.has(matchedId)) {
53346
- result4.set(matchedId, "");
53347
- }
53348
- }
53349
- if (result4.size === taskIds.length) {
53350
- const headerPositions = [];
53351
- const idHeaderRegex2 = /^(#{2,3})\s+Task\s+([^\s].*?)$/gm;
53352
- while ((m = idHeaderRegex2.exec(rawOutput)) !== null) {
53353
- const headerText = m[2].trim();
53354
- const matchedId = taskIds.find((id) => headerText.includes(id));
53355
- if (matchedId) headerPositions.push({ taskId: matchedId, start: m.index + m[0].length });
53356
- }
53357
- headerPositions.sort((a, b) => a.start - b.start);
53358
- for (let i2 = 0; i2 < headerPositions.length; i2 += 1) {
53359
- const cur = headerPositions[i2];
53360
- const next = headerPositions[i2 + 1];
53361
- const end = next ? rawOutput.lastIndexOf("\n#", next.start) : rawOutput.length;
53362
- const text = rawOutput.slice(cur.start, end > 0 ? end : rawOutput.length).trim();
53363
- result4.set(cur.taskId, text);
53364
- }
53365
- }
53366
- return result4;
53367
- }
53368
- var init_output_splitter = __esm({
53369
- "src/runtime/task-runner/output-splitter.ts"() {
53370
- "use strict";
53371
- }
53372
- });
53373
-
53374
- // src/runtime/workspace-tree.ts
53375
- import * as fs73 from "node:fs/promises";
53376
- import * as path61 from "node:path";
53377
- function formatBytes2(bytes) {
53378
- if (bytes < 1024) return `${bytes}B`;
53379
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
53380
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
53381
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
53382
- }
53383
- function formatAge(seconds2) {
53384
- if (seconds2 < 60) return "just now";
53385
- if (seconds2 < 3600) return `${Math.floor(seconds2 / 60)}m`;
53386
- if (seconds2 < 86400) return `${Math.floor(seconds2 / 3600)}h`;
53387
- if (seconds2 < 86400 * 14) return `${Math.floor(seconds2 / 86400)}d`;
53388
- return `${Math.floor(seconds2 / (86400 * 7))}w`;
53389
- }
53390
- function compareByRecency(a, b) {
53391
- const diff = b.mtimeMs - a.mtimeMs;
53392
- if (diff !== 0) return diff;
53393
- return a.name.localeCompare(b.name);
53394
- }
53395
- function applyDirLimit(children, limit) {
53396
- if (children.length <= limit) {
53397
- return { visible: children, dropped: 0 };
53398
- }
53399
- if (limit <= 1) {
53400
- return {
53401
- visible: children.slice(0, limit),
53402
- dropped: children.length - limit
53403
- };
53404
- }
53405
- const recent = children.slice(0, limit - 1);
53406
- const oldest = children[children.length - 1];
53407
- const visible = oldest ? [...recent, oldest] : recent;
53408
- return { visible, dropped: children.length - limit };
53409
- }
53410
- async function readChildren(rootPath, parent, excludedDirs) {
53411
- const dirPath = parent.relativePath ? path61.join(rootPath, parent.relativePath) : rootPath;
53412
- let names;
53413
- try {
53414
- names = await fs73.readdir(dirPath);
53415
- } catch {
53416
- return [];
53417
- }
53418
- const nodes = await Promise.all(
53419
- names.map(async (name) => {
53420
- if (name.startsWith(".")) return null;
53421
- const relativePath = parent.relativePath ? `${parent.relativePath}/${name}` : name;
53422
- const absolutePath = path61.join(rootPath, relativePath);
53423
- try {
53424
- const stat2 = await fs73.stat(absolutePath);
53425
- if (stat2.isDirectory() && excludedDirs.has(name)) return null;
53426
- return {
53427
- name,
53428
- relativePath,
53429
- depth: parent.depth + 1,
53430
- isDirectory: stat2.isDirectory(),
53431
- mtimeMs: stat2.mtimeMs,
53432
- size: stat2.size,
53433
- children: [],
53434
- droppedChildCount: 0
53435
- };
53436
- } catch {
53437
- return null;
53438
- }
53439
- })
53440
- );
53441
- return nodes.filter((n) => n !== null).sort(compareByRecency);
53442
- }
53443
- async function collectTree(rootPath, maxDepth, dirLimit, excludedDirs) {
53444
- const rootStat = await fs73.stat(rootPath);
53445
- const root = {
53446
- name: ".",
53447
- relativePath: "",
53448
- depth: 0,
53449
- isDirectory: true,
53450
- mtimeMs: rootStat.mtimeMs,
53451
- size: rootStat.size,
53452
- children: [],
53453
- droppedChildCount: 0
53454
- };
53455
- let truncated = false;
53456
- const queue = [root];
53457
- let cursor = 0;
53458
- while (cursor < queue.length) {
53459
- const parent = queue[cursor];
53460
- cursor += 1;
53461
- if (!parent || parent.depth >= maxDepth) continue;
53462
- const children = await readChildren(rootPath, parent, excludedDirs);
53463
- const { visible, dropped } = applyDirLimit(children, dirLimit);
53464
- parent.children = visible;
53465
- parent.droppedChildCount = dropped;
53466
- if (dropped > 0) truncated = true;
53467
- for (const child of visible) {
53468
- if (child.isDirectory) queue.push(child);
53469
- }
53470
- }
53471
- return { root, truncated };
53472
- }
53473
- function collectLines(node, nowMs3, lines) {
53474
- if (node.depth === 0) {
53475
- lines.push({ text: ".", depth: 0, isRoot: true });
53476
- } else {
53477
- const indent = " ".repeat(node.depth);
53478
- const suffix = node.isDirectory ? "/" : "";
53479
- const label = `${indent}- ${node.name}${suffix}`;
53480
- if (node.isDirectory) {
53481
- lines.push({ text: label, depth: node.depth, isRoot: false });
53482
- } else {
53483
- const ageSeconds = Math.max(0, Math.floor((nowMs3 - node.mtimeMs) / 1e3));
53484
- const size = formatBytes2(node.size);
53485
- const age = formatAge(ageSeconds);
53486
- lines.push({
53487
- text: `${label} ${size} ${age}`,
53488
- depth: node.depth,
53489
- isRoot: false
53490
- });
53491
- }
53492
- }
53493
- if (node.droppedChildCount > 0) {
53494
- const recentChildren = node.children.slice(0, -1);
53495
- const oldestChild = node.children[node.children.length - 1];
53496
- for (const child of recentChildren) collectLines(child, nowMs3, lines);
53497
- const childDepth = node.depth + 1;
53498
- const indent = " ".repeat(childDepth);
53499
- lines.push({
53500
- text: `${indent}- \u2026 ${node.droppedChildCount} more`,
53501
- depth: childDepth,
53502
- isRoot: false
53503
- });
53504
- if (oldestChild) collectLines(oldestChild, nowMs3, lines);
53505
- } else {
53506
- for (const child of node.children) collectLines(child, nowMs3, lines);
53507
- }
53508
- }
53509
- function applyLineCap(lines, cap) {
53510
- if (lines.length <= cap) return { lines, elided: 0 };
53511
- const target = Math.max(1, cap - 1);
53512
- const removeCount = lines.length - target;
53513
- const removable = lines.map((line4, index) => ({ line: line4, index })).filter((item) => !item.line.isRoot).sort((a, b) => b.line.depth - a.line.depth || b.index - a.index).slice(0, removeCount);
53514
- if (removable.length === 0) return { lines, elided: 0 };
53515
- const removedIndexes = new Set(removable.map((item) => item.index));
53516
- const kept = lines.filter((_, index) => !removedIndexes.has(index));
53517
- kept.push({
53518
- text: `\u2026 (${removable.length} lines elided)`,
53519
- depth: 0,
53520
- isRoot: false
53521
- });
53522
- return { lines: kept, elided: removable.length };
53523
- }
53524
- function treeCacheKey(cwd, options) {
53525
- return `${path61.resolve(cwd)}|${options?.maxDepth ?? ""}|${options?.dirLimit ?? ""}|${options?.lineCap ?? ""}`;
53526
- }
53527
- async function buildWorkspaceTree(cwd, options) {
53528
- const rootPath = path61.resolve(cwd);
53529
- const cacheKey2 = treeCacheKey(cwd, options);
53530
- const cached = treeCache.get(cacheKey2);
53531
- if (cached && cached.expiresAt > Date.now()) {
53532
- return cached.tree;
53533
- }
53534
- try {
53535
- const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
53536
- const dirLimit = options?.dirLimit ?? DEFAULT_DIR_LIMIT;
53537
- const lineCap = options?.lineCap ?? DEFAULT_LINE_CAP;
53538
- const excludedDirs = options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS;
53539
- const { root, truncated: dirTruncated } = await collectTree(rootPath, maxDepth, dirLimit, excludedDirs);
53540
- const nowMs3 = Date.now();
53541
- const lines = [];
53542
- collectLines(root, nowMs3, lines);
53543
- const { lines: capped, elided } = applyLineCap(lines, lineCap);
53544
- const rendered = capped.map((l) => l.text).join("\n");
53545
- const result4 = {
53546
- rootPath,
53547
- rendered,
53548
- truncated: dirTruncated || elided > 0,
53549
- totalLines: capped.length
53550
- };
53551
- treeCache.set(cacheKey2, {
53552
- tree: result4,
53553
- expiresAt: Date.now() + TREE_CACHE_TTL_MS
53554
- });
53555
- return result4;
53556
- } catch {
53557
- return emptyResult(rootPath);
53558
- }
53559
- }
53560
- var DEFAULT_MAX_DEPTH, DEFAULT_DIR_LIMIT, DEFAULT_LINE_CAP, DEFAULT_EXCLUDED_DIRS, emptyResult, TREE_CACHE_TTL_MS, treeCache;
53561
- var init_workspace_tree = __esm({
53562
- "src/runtime/workspace-tree.ts"() {
53563
- "use strict";
53564
- DEFAULT_MAX_DEPTH = 3;
53565
- DEFAULT_DIR_LIMIT = 12;
53566
- DEFAULT_LINE_CAP = 120;
53567
- DEFAULT_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
53568
- "node_modules",
53569
- ".git",
53570
- ".next",
53571
- "dist",
53572
- "build",
53573
- "target",
53574
- ".venv",
53575
- ".cache",
53576
- ".turbo"
53577
- ]);
53578
- emptyResult = (rootPath) => ({
53579
- rootPath,
53580
- rendered: "",
53581
- truncated: false,
53582
- totalLines: 0
53583
- });
53584
- TREE_CACHE_TTL_MS = 3e4;
53585
- treeCache = /* @__PURE__ */ new Map();
53586
- }
53587
- });
53588
-
53589
- // src/runtime/run-coalesced-task-group.ts
53590
- async function runCoalescedTaskGroup(input) {
53591
- const { manifest, groupTasks, step, agent, signal, executeWorkers } = input;
53592
- const groupId = groupTasks.map((t2) => t2.id).join("+");
53593
- const firstTask = groupTasks[0];
53594
- const taskIds = groupTasks.map((t2) => t2.id);
53595
- let updatedTasks = input.tasks.map((t2) => {
53596
- if (taskIds.includes(t2.id) && t2.status !== "running") {
53597
- return { ...t2, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString() };
53598
- }
53599
- return t2;
53600
- });
53601
- saveRunTasks(manifest, updatedTasks);
53602
- await appendEventAsync(manifest.eventsPath, {
53603
- type: "task.coalesced_dispatch_start",
53604
- runId: manifest.runId,
53605
- message: `Dispatching ${groupTasks.length} coalesced tasks in 1 worker (role=${firstTask.role}, cwd=${firstTask.cwd})`,
53606
- data: { groupId, role: firstTask.role, cwd: firstTask.cwd, taskIds }
53607
- });
53608
- const combinedPrompt = await buildCoalescedPrompt(manifest, step, groupTasks, agent);
53609
- updatedTasks = updatedTasks.map((t2) => {
53610
- if (!taskIds.includes(t2.id)) return t2;
53611
- return {
53612
- ...t2,
53613
- heartbeat: t2.heartbeat ?? createWorkerHeartbeat(t2.id)
53614
- };
53615
- });
53616
- saveRunTasks(manifest, updatedTasks);
53617
- let rawOutput = "";
53618
- let success = false;
53619
- if (!executeWorkers) {
53620
- rawOutput = buildScaffoldOutput(groupTasks);
53621
- success = true;
53622
- } else {
53623
- const heartbeatTimer = setInterval(() => {
53624
- const now = (/* @__PURE__ */ new Date()).toISOString();
53625
- updatedTasks = updatedTasks.map((t2) => {
53626
- if (!taskIds.includes(t2.id)) return t2;
53627
- return {
53628
- ...t2,
53629
- heartbeat: touchWorkerHeartbeat(t2.heartbeat ?? createWorkerHeartbeat(t2.id), { alive: true })
53630
- };
53631
- });
53632
- try {
53633
- saveRunTasks(manifest, updatedTasks);
53634
- } catch {
53635
- }
53636
- }, 15e3);
53637
- try {
53638
- const result4 = await runChildPi({
53639
- cwd: firstTask.cwd,
53640
- task: combinedPrompt,
53641
- agent,
53642
- signal,
53643
- excludeContextBash: true,
53644
- maxTurns: 5,
53645
- onJsonEvent: (e) => input.onJsonEvent?.(firstTask.id, manifest.runId, e)
53646
- });
53647
- rawOutput = result4.rawFinalText ?? result4.stdout ?? "";
53648
- success = result4.exitStatus?.exitCode === 0;
53649
- } catch (err2) {
53650
- rawOutput = `Worker dispatch failed: ${err2 instanceof Error ? err2.message : String(err2)}`;
53651
- success = false;
53652
- } finally {
53653
- clearInterval(heartbeatTimer);
53654
- }
53655
- }
53656
- const split = splitCoalescedOutput(rawOutput, taskIds);
53657
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
53658
- const newArtifacts = [];
53659
- updatedTasks = updatedTasks.map((t2) => {
53660
- if (!taskIds.includes(t2.id)) return t2;
53661
- const entry = split.find((s) => s.taskId === t2.id);
53662
- const ok = success && Boolean(entry?.text);
53663
- const text = entry?.text ?? rawOutput;
53664
- const resultArtifact = writeArtifact(manifest.artifactsRoot, {
53665
- kind: "result",
53666
- relativePath: `results/${t2.id}.txt`,
53667
- content: text,
53668
- producer: t2.id
53669
- });
53670
- newArtifacts.push(resultArtifact);
53671
- return {
53672
- ...t2,
53673
- status: ok ? "completed" : "failed",
53674
- finishedAt,
53675
- result: {
53676
- text,
53677
- producer: groupId,
53678
- strategy: entry?.strategy ?? "broadcast"
53679
- },
53680
- resultArtifact
53681
- };
53682
- });
53683
- saveRunTasks(manifest, updatedTasks);
53684
- let updatedManifest = {
53685
- ...manifest,
53686
- artifacts: mergeArtifacts([...manifest.artifacts, ...newArtifacts])
53687
- };
53688
- if (success) {
53689
- updatedManifest = updateRunStatus(updatedManifest, "running");
53690
- }
53691
- await appendEventAsync(updatedManifest.eventsPath, {
53692
- type: "task.coalesced_dispatch_end",
53693
- runId: manifest.runId,
53694
- message: `Coalesced dispatch ${success ? "completed" : "failed"} (${taskIds.length} tasks, ${split[0]?.strategy ?? "broadcast"} split)`,
53695
- data: { groupId, taskIds, success, strategy: split[0]?.strategy }
53696
- });
53697
- return { manifest: updatedManifest, tasks: updatedTasks, taskIds, rawOutput, success };
53698
- }
53699
- async function buildCoalescedPrompt(manifest, step, groupTasks, agent) {
53700
- const tree = await buildWorkspaceTree(groupTasks[0].cwd);
53701
- const treeBlock = tree.rendered ? `# Workspace Structure
53702
- ${tree.rendered}` : "";
53703
- const roleInstructions = permissionForRole(groupTasks[0].role) === "read_only" ? `You are running in READ-ONLY mode. Do not create, modify, delete, or move files. Emit your findings as TEXT in your final output.` : "";
53704
- const taskBlocks = groupTasks.map((task, idx) => {
53705
- return [
53706
- `### Task ${idx + 1} of ${groupTasks.length} (id: ${task.id})`,
53707
- `Step: ${step.id}`,
53708
- `Role: ${step.role}`,
53709
- `Task: ${step.task.replaceAll("{goal}", manifest.goal)}`
53710
- ].join("\n");
53711
- }).join("\n\n---\n\n");
53712
- const outputInstructions = groupTasks.map((task) => `<<<TASK_RESULT:${task.id}>>>`).join(" ... ");
53713
- return [
53714
- "# pi-crew Coalesced Worker Prompt",
53715
- `Run ID: ${manifest.runId}`,
53716
- `Team: ${manifest.team}`,
53717
- `Workflow: ${manifest.workflow ?? "(none)"}`,
53718
- `Goal: ${manifest.goal}`,
53719
- `Tasks in this batch: ${groupTasks.length}`,
53720
- ``,
53721
- roleInstructions,
53722
- ``,
53723
- treeBlock,
53724
- ``,
53725
- `# Your Tasks`,
53726
- `Complete ALL ${groupTasks.length} tasks below. For each, structure your final output using the delimiters shown.`,
53727
- ``,
53728
- taskBlocks,
53729
- ``,
53730
- `# Output Format (CRITICAL)`,
53731
- `After completing all tasks, structure your final output using these delimiters:`,
53732
- ``,
53733
- outputInstructions,
53734
- ``,
53735
- `Wrap each task's result between the start and end delimiters:`,
53736
- `<<<TASK_RESULT:{taskId}>>>`,
53737
- `...your result for this task...`,
53738
- `<<<END_TASK_RESULT>>>`,
53739
- ``,
53740
- `If delimiters don't fit your workflow, use \`### Task N of M\` headings and we'll parse those instead.`
53741
- ].filter(Boolean).join("\n");
53742
- }
53743
- function buildScaffoldOutput(groupTasks) {
53744
- return groupTasks.map(
53745
- (task, idx) => `<<<TASK_RESULT:${task.id}>>>
53746
- Scaffold result for task ${idx + 1} of ${groupTasks.length}: ${task.id}
53747
- <<<END_TASK_RESULT>>>`
53748
- ).join("\n\n");
53749
- }
53750
- var init_run_coalesced_task_group = __esm({
53751
- "src/runtime/run-coalesced-task-group.ts"() {
53752
- "use strict";
53753
- init_role_permission();
53754
- init_child_pi();
53755
- init_output_splitter();
53756
- init_workspace_tree();
53757
- init_state_store();
53758
- init_artifact_store();
53759
- init_event_log();
53760
- init_worker_heartbeat();
53761
- init_team_runner_artifacts();
53762
- }
53763
- });
53764
-
53765
53367
  // src/runtime/concurrency.ts
53766
53368
  function defaultWorkflowConcurrency(workflowName, workflowMaxConcurrency) {
53767
53369
  if (workflowMaxConcurrency !== void 0) return workflowMaxConcurrency;
@@ -54023,12 +53625,12 @@ var init_tool_output_pruner = __esm({
54023
53625
  });
54024
53626
 
54025
53627
  // src/runtime/task-output-context.ts
54026
- import * as fs74 from "node:fs";
54027
- import * as path62 from "node:path";
53628
+ import * as fs73 from "node:fs";
53629
+ import * as path61 from "node:path";
54028
53630
  function containedExists(filePath, baseDir) {
54029
53631
  try {
54030
53632
  const safePath = baseDir ? resolveRealContainedPath(baseDir, filePath) : filePath;
54031
- return fs74.existsSync(safePath);
53633
+ return fs73.existsSync(safePath);
54032
53634
  } catch {
54033
53635
  return false;
54034
53636
  }
@@ -54038,12 +53640,12 @@ function safeTeeName(taskId, artifactName) {
54038
53640
  return `${safe(taskId)}-${safe(artifactName)}.full.txt`;
54039
53641
  }
54040
53642
  function teePathForArtifact(artifactsRoot, taskId, artifactName) {
54041
- return path62.join(artifactsRoot, "tee", safeTeeName(taskId, artifactName));
53643
+ return path61.join(artifactsRoot, "tee", safeTeeName(taskId, artifactName));
54042
53644
  }
54043
53645
  function writeTeeFile(fullOutputPath, content) {
54044
53646
  try {
54045
- fs74.mkdirSync(path62.dirname(fullOutputPath), { recursive: true });
54046
- fs74.writeFileSync(fullOutputPath, content, "utf-8");
53647
+ fs73.mkdirSync(path61.dirname(fullOutputPath), { recursive: true });
53648
+ fs73.writeFileSync(fullOutputPath, content, "utf-8");
54047
53649
  return true;
54048
53650
  } catch {
54049
53651
  return false;
@@ -54053,7 +53655,7 @@ function readIfSmallWithTee(filePath, opts = {}) {
54053
53655
  const maxChars = MAX_RESULT_INLINE_BYTES;
54054
53656
  try {
54055
53657
  const safePath = opts.baseDir ? resolveRealContainedPath(opts.baseDir, filePath) : filePath;
54056
- const content = fs74.readFileSync(safePath, "utf-8");
53658
+ const content = fs73.readFileSync(safePath, "utf-8");
54057
53659
  if (content.length > maxChars) {
54058
53660
  let fullOutputPath;
54059
53661
  if (opts.tee && content.length > maxChars * TEE_THRESHOLD_MULTIPLIER) {
@@ -54087,15 +53689,15 @@ function readIfSmall(filePath, baseDir) {
54087
53689
  }
54088
53690
  function safeSharedName(name) {
54089
53691
  const normalized = name.replaceAll("\\", "/").replace(/^\.\/+/, "");
54090
- if (!normalized || normalized.split("/").some((segment) => segment === "..") || path62.isAbsolute(normalized))
53692
+ if (!normalized || normalized.split("/").some((segment) => segment === "..") || path61.isAbsolute(normalized))
54091
53693
  throw new Error(`Invalid shared artifact name: ${name}`);
54092
53694
  return normalized;
54093
53695
  }
54094
53696
  function sharedPath(manifest, name) {
54095
- const sharedRoot = path62.resolve(manifest.artifactsRoot, "shared");
54096
- const resolved = path62.resolve(sharedRoot, safeSharedName(name));
54097
- const relative8 = path62.relative(sharedRoot, resolved);
54098
- if (relative8.startsWith("..") || path62.isAbsolute(relative8)) throw new Error(`Invalid shared artifact name: ${name}`);
53697
+ const sharedRoot = path61.resolve(manifest.artifactsRoot, "shared");
53698
+ const resolved = path61.resolve(sharedRoot, safeSharedName(name));
53699
+ const relative8 = path61.relative(sharedRoot, resolved);
53700
+ if (relative8.startsWith("..") || path61.isAbsolute(relative8)) throw new Error(`Invalid shared artifact name: ${name}`);
54099
53701
  return resolved;
54100
53702
  }
54101
53703
  function tryParseJson(text) {
@@ -54110,7 +53712,7 @@ function listTaskArtifacts(manifest, taskId) {
54110
53712
  const produced = manifest.artifacts.filter((a) => a.producer === taskId);
54111
53713
  if (produced.length === 0) return void 0;
54112
53714
  return produced.map((a) => {
54113
- const relative8 = path62.relative(manifest.artifactsRoot, a.path);
53715
+ const relative8 = path61.relative(manifest.artifactsRoot, a.path);
54114
53716
  return relative8.startsWith("..") ? a.path : relative8;
54115
53717
  });
54116
53718
  }
@@ -54168,7 +53770,7 @@ function pruneSharedReads(reads, dependencies, artifactsRoot) {
54168
53770
  for (const artifact of produced) {
54169
53771
  if (typeof artifact !== "string") continue;
54170
53772
  fileEdits.push({
54171
- target: path62.resolve(artifactsRoot, artifact),
53773
+ target: path61.resolve(artifactsRoot, artifact),
54172
53774
  index: reads.length + depIndex
54173
53775
  });
54174
53776
  }
@@ -54221,7 +53823,7 @@ function collectDependencyOutputContext(manifest, tasks, task, step) {
54221
53823
  const filePath = sharedPath(manifest, name);
54222
53824
  const teePath = teePathForArtifact(manifest.artifactsRoot, task.id, name);
54223
53825
  const teeResult = readIfSmallWithTee(filePath, {
54224
- baseDir: path62.resolve(manifest.artifactsRoot, "shared"),
53826
+ baseDir: path61.resolve(manifest.artifactsRoot, "shared"),
54225
53827
  tee: { fullOutputPath: teePath }
54226
53828
  });
54227
53829
  if (teeResult === void 0) return { name, path: filePath, content: "" };
@@ -54733,6 +54335,502 @@ var init_retry_executor = __esm({
54733
54335
  }
54734
54336
  });
54735
54337
 
54338
+ // src/runtime/task-runner/output-splitter.ts
54339
+ function splitCoalescedOutput(rawOutput, taskIds) {
54340
+ if (taskIds.length === 0) return [];
54341
+ if (taskIds.length === 1) {
54342
+ return [{ taskId: taskIds[0], text: rawOutput, strategy: "delimiter" }];
54343
+ }
54344
+ const byId = /* @__PURE__ */ new Map();
54345
+ const delimiterHits = /* @__PURE__ */ new Set();
54346
+ const delimiterRegex = /<<<TASK_RESULT:([^\s>]+)>>>([\s\S]*?)<<<END_TASK_RESULT>>>/g;
54347
+ let match;
54348
+ while ((match = delimiterRegex.exec(rawOutput)) !== null) {
54349
+ const id = match[1];
54350
+ const body = match[2].trim();
54351
+ if (taskIds.includes(id) && !byId.has(id)) {
54352
+ byId.set(id, body);
54353
+ delimiterHits.add(id);
54354
+ }
54355
+ }
54356
+ if (delimiterHits.size === taskIds.length) {
54357
+ return taskIds.map((id) => ({
54358
+ taskId: id,
54359
+ text: byId.get(id) ?? "",
54360
+ strategy: "delimiter"
54361
+ }));
54362
+ }
54363
+ if (delimiterHits.size === 0) {
54364
+ const bySection = parseBySectionHeadings(rawOutput, taskIds);
54365
+ if (bySection.size === taskIds.length) {
54366
+ return taskIds.map((id) => ({
54367
+ taskId: id,
54368
+ text: bySection.get(id) ?? "",
54369
+ strategy: "section"
54370
+ }));
54371
+ }
54372
+ }
54373
+ return taskIds.map((id) => ({
54374
+ taskId: id,
54375
+ text: rawOutput,
54376
+ strategy: "broadcast"
54377
+ }));
54378
+ }
54379
+ function parseBySectionHeadings(rawOutput, taskIds) {
54380
+ const result4 = /* @__PURE__ */ new Map();
54381
+ const numberedRegex = /^(#{2,3})\s+Task\s+(\d+)\s+of\s+\d+.*$/gm;
54382
+ const sections = [];
54383
+ let m;
54384
+ while ((m = numberedRegex.exec(rawOutput)) !== null) {
54385
+ sections.push({ num: Number(m[2]), start: m.index + m[0].length });
54386
+ }
54387
+ if (sections.length >= taskIds.length) {
54388
+ sections.sort((a, b) => a.start - b.start);
54389
+ for (let i2 = 0; i2 < sections.length && i2 < taskIds.length; i2 += 1) {
54390
+ const section2 = sections[i2];
54391
+ const end = sections[i2 + 1]?.start ?? rawOutput.length;
54392
+ const text = rawOutput.slice(section2.start, end).trim();
54393
+ const taskId = taskIds[i2];
54394
+ if (taskId) result4.set(taskId, text);
54395
+ }
54396
+ return result4;
54397
+ }
54398
+ const idHeaderRegex = /^(#{2,3})\s+Task\s+([^\s].*?)$/gm;
54399
+ while ((m = idHeaderRegex.exec(rawOutput)) !== null) {
54400
+ const headerText = m[2].trim();
54401
+ const matchedId = taskIds.find((id) => headerText.includes(id));
54402
+ if (matchedId && !result4.has(matchedId)) {
54403
+ result4.set(matchedId, "");
54404
+ }
54405
+ }
54406
+ if (result4.size === taskIds.length) {
54407
+ const headerPositions = [];
54408
+ const idHeaderRegex2 = /^(#{2,3})\s+Task\s+([^\s].*?)$/gm;
54409
+ while ((m = idHeaderRegex2.exec(rawOutput)) !== null) {
54410
+ const headerText = m[2].trim();
54411
+ const matchedId = taskIds.find((id) => headerText.includes(id));
54412
+ if (matchedId) headerPositions.push({ taskId: matchedId, start: m.index + m[0].length });
54413
+ }
54414
+ headerPositions.sort((a, b) => a.start - b.start);
54415
+ for (let i2 = 0; i2 < headerPositions.length; i2 += 1) {
54416
+ const cur = headerPositions[i2];
54417
+ const next = headerPositions[i2 + 1];
54418
+ const end = next ? rawOutput.lastIndexOf("\n#", next.start) : rawOutput.length;
54419
+ const text = rawOutput.slice(cur.start, end > 0 ? end : rawOutput.length).trim();
54420
+ result4.set(cur.taskId, text);
54421
+ }
54422
+ }
54423
+ return result4;
54424
+ }
54425
+ var init_output_splitter = __esm({
54426
+ "src/runtime/task-runner/output-splitter.ts"() {
54427
+ "use strict";
54428
+ }
54429
+ });
54430
+
54431
+ // src/runtime/team-runner-artifacts.ts
54432
+ function mergeArtifacts(items) {
54433
+ const byPath = /* @__PURE__ */ new Map();
54434
+ for (const item of items) byPath.set(item.path, item);
54435
+ return [...byPath.values()];
54436
+ }
54437
+ var init_team_runner_artifacts = __esm({
54438
+ "src/runtime/team-runner-artifacts.ts"() {
54439
+ "use strict";
54440
+ }
54441
+ });
54442
+
54443
+ // src/runtime/workspace-tree.ts
54444
+ import * as fs74 from "node:fs/promises";
54445
+ import * as path62 from "node:path";
54446
+ function formatBytes2(bytes) {
54447
+ if (bytes < 1024) return `${bytes}B`;
54448
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
54449
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
54450
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
54451
+ }
54452
+ function formatAge(seconds2) {
54453
+ if (seconds2 < 60) return "just now";
54454
+ if (seconds2 < 3600) return `${Math.floor(seconds2 / 60)}m`;
54455
+ if (seconds2 < 86400) return `${Math.floor(seconds2 / 3600)}h`;
54456
+ if (seconds2 < 86400 * 14) return `${Math.floor(seconds2 / 86400)}d`;
54457
+ return `${Math.floor(seconds2 / (86400 * 7))}w`;
54458
+ }
54459
+ function compareByRecency(a, b) {
54460
+ const diff = b.mtimeMs - a.mtimeMs;
54461
+ if (diff !== 0) return diff;
54462
+ return a.name.localeCompare(b.name);
54463
+ }
54464
+ function applyDirLimit(children, limit) {
54465
+ if (children.length <= limit) {
54466
+ return { visible: children, dropped: 0 };
54467
+ }
54468
+ if (limit <= 1) {
54469
+ return {
54470
+ visible: children.slice(0, limit),
54471
+ dropped: children.length - limit
54472
+ };
54473
+ }
54474
+ const recent = children.slice(0, limit - 1);
54475
+ const oldest = children[children.length - 1];
54476
+ const visible = oldest ? [...recent, oldest] : recent;
54477
+ return { visible, dropped: children.length - limit };
54478
+ }
54479
+ async function readChildren(rootPath, parent, excludedDirs) {
54480
+ const dirPath = parent.relativePath ? path62.join(rootPath, parent.relativePath) : rootPath;
54481
+ let names;
54482
+ try {
54483
+ names = await fs74.readdir(dirPath);
54484
+ } catch {
54485
+ return [];
54486
+ }
54487
+ const nodes = await Promise.all(
54488
+ names.map(async (name) => {
54489
+ if (name.startsWith(".")) return null;
54490
+ const relativePath = parent.relativePath ? `${parent.relativePath}/${name}` : name;
54491
+ const absolutePath = path62.join(rootPath, relativePath);
54492
+ try {
54493
+ const stat2 = await fs74.stat(absolutePath);
54494
+ if (stat2.isDirectory() && excludedDirs.has(name)) return null;
54495
+ return {
54496
+ name,
54497
+ relativePath,
54498
+ depth: parent.depth + 1,
54499
+ isDirectory: stat2.isDirectory(),
54500
+ mtimeMs: stat2.mtimeMs,
54501
+ size: stat2.size,
54502
+ children: [],
54503
+ droppedChildCount: 0
54504
+ };
54505
+ } catch {
54506
+ return null;
54507
+ }
54508
+ })
54509
+ );
54510
+ return nodes.filter((n) => n !== null).sort(compareByRecency);
54511
+ }
54512
+ async function collectTree(rootPath, maxDepth, dirLimit, excludedDirs) {
54513
+ const rootStat = await fs74.stat(rootPath);
54514
+ const root = {
54515
+ name: ".",
54516
+ relativePath: "",
54517
+ depth: 0,
54518
+ isDirectory: true,
54519
+ mtimeMs: rootStat.mtimeMs,
54520
+ size: rootStat.size,
54521
+ children: [],
54522
+ droppedChildCount: 0
54523
+ };
54524
+ let truncated = false;
54525
+ const queue = [root];
54526
+ let cursor = 0;
54527
+ while (cursor < queue.length) {
54528
+ const parent = queue[cursor];
54529
+ cursor += 1;
54530
+ if (!parent || parent.depth >= maxDepth) continue;
54531
+ const children = await readChildren(rootPath, parent, excludedDirs);
54532
+ const { visible, dropped } = applyDirLimit(children, dirLimit);
54533
+ parent.children = visible;
54534
+ parent.droppedChildCount = dropped;
54535
+ if (dropped > 0) truncated = true;
54536
+ for (const child of visible) {
54537
+ if (child.isDirectory) queue.push(child);
54538
+ }
54539
+ }
54540
+ return { root, truncated };
54541
+ }
54542
+ function collectLines(node, nowMs3, lines) {
54543
+ if (node.depth === 0) {
54544
+ lines.push({ text: ".", depth: 0, isRoot: true });
54545
+ } else {
54546
+ const indent = " ".repeat(node.depth);
54547
+ const suffix = node.isDirectory ? "/" : "";
54548
+ const label = `${indent}- ${node.name}${suffix}`;
54549
+ if (node.isDirectory) {
54550
+ lines.push({ text: label, depth: node.depth, isRoot: false });
54551
+ } else {
54552
+ const ageSeconds = Math.max(0, Math.floor((nowMs3 - node.mtimeMs) / 1e3));
54553
+ const size = formatBytes2(node.size);
54554
+ const age = formatAge(ageSeconds);
54555
+ lines.push({
54556
+ text: `${label} ${size} ${age}`,
54557
+ depth: node.depth,
54558
+ isRoot: false
54559
+ });
54560
+ }
54561
+ }
54562
+ if (node.droppedChildCount > 0) {
54563
+ const recentChildren = node.children.slice(0, -1);
54564
+ const oldestChild = node.children[node.children.length - 1];
54565
+ for (const child of recentChildren) collectLines(child, nowMs3, lines);
54566
+ const childDepth = node.depth + 1;
54567
+ const indent = " ".repeat(childDepth);
54568
+ lines.push({
54569
+ text: `${indent}- \u2026 ${node.droppedChildCount} more`,
54570
+ depth: childDepth,
54571
+ isRoot: false
54572
+ });
54573
+ if (oldestChild) collectLines(oldestChild, nowMs3, lines);
54574
+ } else {
54575
+ for (const child of node.children) collectLines(child, nowMs3, lines);
54576
+ }
54577
+ }
54578
+ function applyLineCap(lines, cap) {
54579
+ if (lines.length <= cap) return { lines, elided: 0 };
54580
+ const target = Math.max(1, cap - 1);
54581
+ const removeCount = lines.length - target;
54582
+ const removable = lines.map((line4, index) => ({ line: line4, index })).filter((item) => !item.line.isRoot).sort((a, b) => b.line.depth - a.line.depth || b.index - a.index).slice(0, removeCount);
54583
+ if (removable.length === 0) return { lines, elided: 0 };
54584
+ const removedIndexes = new Set(removable.map((item) => item.index));
54585
+ const kept = lines.filter((_, index) => !removedIndexes.has(index));
54586
+ kept.push({
54587
+ text: `\u2026 (${removable.length} lines elided)`,
54588
+ depth: 0,
54589
+ isRoot: false
54590
+ });
54591
+ return { lines: kept, elided: removable.length };
54592
+ }
54593
+ function treeCacheKey(cwd, options) {
54594
+ return `${path62.resolve(cwd)}|${options?.maxDepth ?? ""}|${options?.dirLimit ?? ""}|${options?.lineCap ?? ""}`;
54595
+ }
54596
+ async function buildWorkspaceTree(cwd, options) {
54597
+ const rootPath = path62.resolve(cwd);
54598
+ const cacheKey2 = treeCacheKey(cwd, options);
54599
+ const cached = treeCache.get(cacheKey2);
54600
+ if (cached && cached.expiresAt > Date.now()) {
54601
+ return cached.tree;
54602
+ }
54603
+ try {
54604
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
54605
+ const dirLimit = options?.dirLimit ?? DEFAULT_DIR_LIMIT;
54606
+ const lineCap = options?.lineCap ?? DEFAULT_LINE_CAP;
54607
+ const excludedDirs = options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS;
54608
+ const { root, truncated: dirTruncated } = await collectTree(rootPath, maxDepth, dirLimit, excludedDirs);
54609
+ const nowMs3 = Date.now();
54610
+ const lines = [];
54611
+ collectLines(root, nowMs3, lines);
54612
+ const { lines: capped, elided } = applyLineCap(lines, lineCap);
54613
+ const rendered = capped.map((l) => l.text).join("\n");
54614
+ const result4 = {
54615
+ rootPath,
54616
+ rendered,
54617
+ truncated: dirTruncated || elided > 0,
54618
+ totalLines: capped.length
54619
+ };
54620
+ treeCache.set(cacheKey2, {
54621
+ tree: result4,
54622
+ expiresAt: Date.now() + TREE_CACHE_TTL_MS
54623
+ });
54624
+ return result4;
54625
+ } catch {
54626
+ return emptyResult(rootPath);
54627
+ }
54628
+ }
54629
+ var DEFAULT_MAX_DEPTH, DEFAULT_DIR_LIMIT, DEFAULT_LINE_CAP, DEFAULT_EXCLUDED_DIRS, emptyResult, TREE_CACHE_TTL_MS, treeCache;
54630
+ var init_workspace_tree = __esm({
54631
+ "src/runtime/workspace-tree.ts"() {
54632
+ "use strict";
54633
+ DEFAULT_MAX_DEPTH = 3;
54634
+ DEFAULT_DIR_LIMIT = 12;
54635
+ DEFAULT_LINE_CAP = 120;
54636
+ DEFAULT_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
54637
+ "node_modules",
54638
+ ".git",
54639
+ ".next",
54640
+ "dist",
54641
+ "build",
54642
+ "target",
54643
+ ".venv",
54644
+ ".cache",
54645
+ ".turbo"
54646
+ ]);
54647
+ emptyResult = (rootPath) => ({
54648
+ rootPath,
54649
+ rendered: "",
54650
+ truncated: false,
54651
+ totalLines: 0
54652
+ });
54653
+ TREE_CACHE_TTL_MS = 3e4;
54654
+ treeCache = /* @__PURE__ */ new Map();
54655
+ }
54656
+ });
54657
+
54658
+ // src/runtime/run-coalesced-task-group.ts
54659
+ async function runCoalescedTaskGroup(input) {
54660
+ const { manifest, groupTasks, step, agent, signal, executeWorkers } = input;
54661
+ const groupId = groupTasks.map((t2) => t2.id).join("+");
54662
+ const firstTask = groupTasks[0];
54663
+ const taskIds = groupTasks.map((t2) => t2.id);
54664
+ let updatedTasks = input.tasks.map((t2) => {
54665
+ if (taskIds.includes(t2.id) && t2.status !== "running") {
54666
+ return { ...t2, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString() };
54667
+ }
54668
+ return t2;
54669
+ });
54670
+ saveRunTasks(manifest, updatedTasks);
54671
+ await appendEventAsync(manifest.eventsPath, {
54672
+ type: "task.coalesced_dispatch_start",
54673
+ runId: manifest.runId,
54674
+ message: `Dispatching ${groupTasks.length} coalesced tasks in 1 worker (role=${firstTask.role}, cwd=${firstTask.cwd})`,
54675
+ data: { groupId, role: firstTask.role, cwd: firstTask.cwd, taskIds }
54676
+ });
54677
+ const combinedPrompt = await buildCoalescedPrompt(manifest, step, groupTasks, agent);
54678
+ updatedTasks = updatedTasks.map((t2) => {
54679
+ if (!taskIds.includes(t2.id)) return t2;
54680
+ return {
54681
+ ...t2,
54682
+ heartbeat: t2.heartbeat ?? createWorkerHeartbeat(t2.id)
54683
+ };
54684
+ });
54685
+ saveRunTasks(manifest, updatedTasks);
54686
+ let rawOutput = "";
54687
+ let success = false;
54688
+ if (!executeWorkers) {
54689
+ rawOutput = buildScaffoldOutput(groupTasks);
54690
+ success = true;
54691
+ } else {
54692
+ const heartbeatTimer = setInterval(() => {
54693
+ const now = (/* @__PURE__ */ new Date()).toISOString();
54694
+ updatedTasks = updatedTasks.map((t2) => {
54695
+ if (!taskIds.includes(t2.id)) return t2;
54696
+ return {
54697
+ ...t2,
54698
+ heartbeat: touchWorkerHeartbeat(t2.heartbeat ?? createWorkerHeartbeat(t2.id), { alive: true })
54699
+ };
54700
+ });
54701
+ try {
54702
+ saveRunTasks(manifest, updatedTasks);
54703
+ } catch {
54704
+ }
54705
+ }, 15e3);
54706
+ try {
54707
+ const result4 = await runChildPi({
54708
+ cwd: firstTask.cwd,
54709
+ task: combinedPrompt,
54710
+ agent,
54711
+ signal,
54712
+ excludeContextBash: true,
54713
+ maxTurns: 5,
54714
+ onJsonEvent: (e) => input.onJsonEvent?.(firstTask.id, manifest.runId, e)
54715
+ });
54716
+ rawOutput = result4.rawFinalText ?? result4.stdout ?? "";
54717
+ success = result4.exitStatus?.exitCode === 0;
54718
+ } catch (err2) {
54719
+ rawOutput = `Worker dispatch failed: ${err2 instanceof Error ? err2.message : String(err2)}`;
54720
+ success = false;
54721
+ } finally {
54722
+ clearInterval(heartbeatTimer);
54723
+ }
54724
+ }
54725
+ const split = splitCoalescedOutput(rawOutput, taskIds);
54726
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
54727
+ const newArtifacts = [];
54728
+ updatedTasks = updatedTasks.map((t2) => {
54729
+ if (!taskIds.includes(t2.id)) return t2;
54730
+ const entry = split.find((s) => s.taskId === t2.id);
54731
+ const ok = success && Boolean(entry?.text);
54732
+ const text = entry?.text ?? rawOutput;
54733
+ const resultArtifact = writeArtifact(manifest.artifactsRoot, {
54734
+ kind: "result",
54735
+ relativePath: `results/${t2.id}.txt`,
54736
+ content: text,
54737
+ producer: t2.id
54738
+ });
54739
+ newArtifacts.push(resultArtifact);
54740
+ return {
54741
+ ...t2,
54742
+ status: ok ? "completed" : "failed",
54743
+ finishedAt,
54744
+ result: {
54745
+ text,
54746
+ producer: groupId,
54747
+ strategy: entry?.strategy ?? "broadcast"
54748
+ },
54749
+ resultArtifact
54750
+ };
54751
+ });
54752
+ saveRunTasks(manifest, updatedTasks);
54753
+ let updatedManifest = {
54754
+ ...manifest,
54755
+ artifacts: mergeArtifacts([...manifest.artifacts, ...newArtifacts])
54756
+ };
54757
+ if (success) {
54758
+ updatedManifest = updateRunStatus(updatedManifest, "running");
54759
+ }
54760
+ await appendEventAsync(updatedManifest.eventsPath, {
54761
+ type: "task.coalesced_dispatch_end",
54762
+ runId: manifest.runId,
54763
+ message: `Coalesced dispatch ${success ? "completed" : "failed"} (${taskIds.length} tasks, ${split[0]?.strategy ?? "broadcast"} split)`,
54764
+ data: { groupId, taskIds, success, strategy: split[0]?.strategy }
54765
+ });
54766
+ return { manifest: updatedManifest, tasks: updatedTasks, taskIds, rawOutput, success };
54767
+ }
54768
+ async function buildCoalescedPrompt(manifest, step, groupTasks, agent) {
54769
+ const tree = await buildWorkspaceTree(groupTasks[0].cwd);
54770
+ const treeBlock = tree.rendered ? `# Workspace Structure
54771
+ ${tree.rendered}` : "";
54772
+ const roleInstructions = permissionForRole(groupTasks[0].role) === "read_only" ? `You are running in READ-ONLY mode. Do not create, modify, delete, or move files. Emit your findings as TEXT in your final output.` : "";
54773
+ const taskBlocks = groupTasks.map((task, idx) => {
54774
+ return [
54775
+ `### Task ${idx + 1} of ${groupTasks.length} (id: ${task.id})`,
54776
+ `Step: ${step.id}`,
54777
+ `Role: ${step.role}`,
54778
+ `Task: ${step.task.replaceAll("{goal}", manifest.goal)}`
54779
+ ].join("\n");
54780
+ }).join("\n\n---\n\n");
54781
+ const outputInstructions = groupTasks.map((task) => `<<<TASK_RESULT:${task.id}>>>`).join(" ... ");
54782
+ return [
54783
+ "# pi-crew Coalesced Worker Prompt",
54784
+ `Run ID: ${manifest.runId}`,
54785
+ `Team: ${manifest.team}`,
54786
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
54787
+ `Goal: ${manifest.goal}`,
54788
+ `Tasks in this batch: ${groupTasks.length}`,
54789
+ ``,
54790
+ roleInstructions,
54791
+ ``,
54792
+ treeBlock,
54793
+ ``,
54794
+ `# Your Tasks`,
54795
+ `Complete ALL ${groupTasks.length} tasks below. For each, structure your final output using the delimiters shown.`,
54796
+ ``,
54797
+ taskBlocks,
54798
+ ``,
54799
+ `# Output Format (CRITICAL)`,
54800
+ `After completing all tasks, structure your final output using these delimiters:`,
54801
+ ``,
54802
+ outputInstructions,
54803
+ ``,
54804
+ `Wrap each task's result between the start and end delimiters:`,
54805
+ `<<<TASK_RESULT:{taskId}>>>`,
54806
+ `...your result for this task...`,
54807
+ `<<<END_TASK_RESULT>>>`,
54808
+ ``,
54809
+ `If delimiters don't fit your workflow, use \`### Task N of M\` headings and we'll parse those instead.`
54810
+ ].filter(Boolean).join("\n");
54811
+ }
54812
+ function buildScaffoldOutput(groupTasks) {
54813
+ return groupTasks.map(
54814
+ (task, idx) => `<<<TASK_RESULT:${task.id}>>>
54815
+ Scaffold result for task ${idx + 1} of ${groupTasks.length}: ${task.id}
54816
+ <<<END_TASK_RESULT>>>`
54817
+ ).join("\n\n");
54818
+ }
54819
+ var init_run_coalesced_task_group = __esm({
54820
+ "src/runtime/run-coalesced-task-group.ts"() {
54821
+ "use strict";
54822
+ init_artifact_store();
54823
+ init_event_log();
54824
+ init_state_store();
54825
+ init_child_pi();
54826
+ init_role_permission();
54827
+ init_output_splitter();
54828
+ init_team_runner_artifacts();
54829
+ init_worker_heartbeat();
54830
+ init_workspace_tree();
54831
+ }
54832
+ });
54833
+
54736
54834
  // src/runtime/runtime-policy.ts
54737
54835
  function resolveTaskRuntimeKind(globalKind, role, isolationPolicy, env = process.env) {
54738
54836
  if (globalKind === "scaffold") return "scaffold";
@@ -55719,7 +55817,7 @@ function hashToBase36(content, length) {
55719
55817
  }
55720
55818
  function calculateAdaptiveLength(existingCount, config = DEFAULT_CONFIG) {
55721
55819
  for (let length = config.minLength; length <= config.maxLength; length++) {
55722
- const totalPossibilities = Math.pow(36, length);
55820
+ const totalPossibilities = 36 ** length;
55723
55821
  const probability = 1 - Math.exp(-(existingCount * existingCount) / (2 * totalPossibilities));
55724
55822
  if (probability <= config.maxCollisionProbability) {
55725
55823
  return length;
@@ -56812,7 +56910,8 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56812
56910
  } : updated;
56813
56911
  try {
56814
56912
  return withRunLockSync(manifest, () => {
56815
- retryLoop: for (let attempt = 0; attempt < 100; attempt++) {
56913
+ for (let attempt = 0; attempt < 100; attempt++) {
56914
+ flushPendingAtomicWrites();
56816
56915
  const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
56817
56916
  merged = updateTask(latest, taskWithCheckpoint);
56818
56917
  let currentMtime;
@@ -56823,7 +56922,7 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56823
56922
  }
56824
56923
  if (currentMtime !== baseMtime) {
56825
56924
  baseMtime = currentMtime;
56826
- continue retryLoop;
56925
+ continue;
56827
56926
  }
56828
56927
  let recheckMtime;
56829
56928
  try {
@@ -56833,7 +56932,7 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56833
56932
  }
56834
56933
  if (recheckMtime !== baseMtime) {
56835
56934
  baseMtime = recheckMtime;
56836
- continue retryLoop;
56935
+ continue;
56837
56936
  }
56838
56937
  let preWriteMtime;
56839
56938
  try {
@@ -56843,16 +56942,16 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56843
56942
  }
56844
56943
  if (preWriteMtime !== baseMtime) {
56845
56944
  baseMtime = preWriteMtime;
56846
- continue retryLoop;
56945
+ continue;
56847
56946
  }
56848
- break retryLoop;
56947
+ break;
56849
56948
  }
56850
56949
  if (merged === void 0) {
56851
56950
  logInternalError("persistSingleTaskUpdate", new Error("failed to converge after 50 attempts"));
56852
56951
  throw new Error("persistSingleTaskUpdate: failed to converge after 50 attempts");
56853
56952
  }
56854
56953
  try {
56855
- saveRunTasks(manifest, merged);
56954
+ saveRunTasksCoalesced(manifest, merged);
56856
56955
  } catch (err2) {
56857
56956
  logInternalError("persistSingleTaskUpdate", err2);
56858
56957
  throw err2;
@@ -56884,6 +56983,7 @@ function checkpointTask(manifest, tasks, task, phase, childPid) {
56884
56983
  var init_state_helpers = __esm({
56885
56984
  "src/runtime/task-runner/state-helpers.ts"() {
56886
56985
  "use strict";
56986
+ init_atomic_write();
56887
56987
  init_locks();
56888
56988
  init_state_store();
56889
56989
  init_internal_error();
@@ -60494,10 +60594,8 @@ var init_team_runner = __esm({
60494
60594
  init_usage();
60495
60595
  init_internal_error();
60496
60596
  init_branch_freshness();
60497
- init_team_runner_artifacts();
60498
60597
  init_cancellation();
60499
60598
  init_coalesce_tasks();
60500
- init_run_coalesced_task_group();
60501
60599
  init_concurrency();
60502
60600
  init_crew_agent_records();
60503
60601
  init_crew_hooks();
@@ -60512,6 +60610,7 @@ var init_team_runner = __esm({
60512
60610
  init_recovery_recipes();
60513
60611
  init_retry_executor();
60514
60612
  init_role_permission();
60613
+ init_run_coalesced_task_group();
60515
60614
  init_run_tracker();
60516
60615
  init_runtime_policy();
60517
60616
  init_task_display();
@@ -60519,6 +60618,7 @@ var init_team_runner = __esm({
60519
60618
  init_task_graph_scheduler();
60520
60619
  init_task_output_context();
60521
60620
  init_task_runner();
60621
+ init_team_runner_artifacts();
60522
60622
  init_usage_tracker();
60523
60623
  init_workflow_state();
60524
60624
  init_adaptive_plan();
@@ -68516,7 +68616,6 @@ function tryScanJson(text) {
68516
68616
  try {
68517
68617
  return JSON.parse(candidate);
68518
68618
  } catch {
68519
- continue;
68520
68619
  }
68521
68620
  }
68522
68621
  return void 0;
@@ -68672,7 +68771,7 @@ function makeWorkflowCtx(manifest, opts) {
68672
68771
  const semaphore = new Semaphore(concurrency);
68673
68772
  let finalResult;
68674
68773
  let agentCount = opts.resumedState ? opts.resumedState.agentCount : 0;
68675
- let phaseState = opts.resumedState ? {
68774
+ const phaseState = opts.resumedState ? {
68676
68775
  currentPhase: opts.resumedState.currentPhase,
68677
68776
  phases: [...opts.resumedState.phases]
68678
68777
  } : { currentPhase: void 0, phases: [] };