pi-crew 0.9.17 → 0.9.19

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;
@@ -46506,6 +46604,17 @@ var init_team_tool_schema = __esm({
46506
46604
  // "description-only schema" strict-provider check.
46507
46605
  Type.Any()
46508
46606
  ),
46607
+ analysis: Type.Optional(
46608
+ Type.String({
46609
+ maxLength: 1e5,
46610
+ description: "Inline analysis/context notes from the calling session. Persisted to artifacts/{runId}/shared/analysis.md (audit trail) and auto-injected into any workflow step declaring reads: analysis.md (e.g. builtin 'plan-execute'). Mutually exclusive with analysisPath. Ignored by goal-wrapped runs and chain dispatch in v1."
46611
+ })
46612
+ ),
46613
+ analysisPath: Type.Optional(
46614
+ Type.String({
46615
+ description: "Path to an existing markdown analysis file (resolved within cwd). Copied into shared/analysis.md. Mutually exclusive with analysis."
46616
+ })
46617
+ ),
46509
46618
  focus: Type.Optional(
46510
46619
  Type.String({
46511
46620
  description: "Sub-focus for the doctor action. 'zombies' runs a READ-ONLY scan for orphaned pi-crew sub-agent processes (identified by PI_CREW_KIND=subagent); it never kills and never matches the user's interactive main session."
@@ -47299,7 +47408,6 @@ function snapshotManifests(cwd) {
47299
47408
  const content = fs59.readFileSync(abs);
47300
47409
  snapshot[rel] = createHash6("sha256").update(content).digest("hex");
47301
47410
  } catch {
47302
- continue;
47303
47411
  }
47304
47412
  }
47305
47413
  return snapshot;
@@ -47481,7 +47589,7 @@ function getBackgroundRunnerCommand(runnerPath, cwd, runId, loaderInput = resolv
47481
47589
  };
47482
47590
  }
47483
47591
  async function spawnBackgroundTeamRun(manifest) {
47484
- const runnerPath = path52.join(path52.dirname(fileURLToPath6(import.meta.url)), "background-runner.ts");
47592
+ const runnerPath = path52.join(packageRoot(), "src", "runtime", "background-runner.ts");
47485
47593
  const logPath = path52.join(manifest.stateRoot, "background.log");
47486
47594
  fs60.mkdirSync(manifest.stateRoot, { recursive: true });
47487
47595
  const filteredEnv = sanitizeEnvSecrets(process.env, {
@@ -47571,6 +47679,7 @@ var init_async_runner = __esm({
47571
47679
  init_env_allowlist();
47572
47680
  init_env_filter();
47573
47681
  init_internal_error();
47682
+ init_paths();
47574
47683
  init_orphan_worker_registry();
47575
47684
  init_peer_dep();
47576
47685
  requireFromHere = createRequire3(import.meta.url);
@@ -53140,18 +53249,6 @@ var init_branch_freshness = __esm({
53140
53249
  }
53141
53250
  });
53142
53251
 
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
53252
  // src/runtime/coalesce-tasks.ts
53156
53253
  function planCoalescedGroups(readyTaskIds, tasks, workflow, enabled, maxGroupSize = DEFAULT_MAX_GROUP_SIZE, maxGroupBytes = DEFAULT_MAX_GROUP_BYTES) {
53157
53254
  if (!enabled || readyTaskIds.length === 0) return [];
@@ -53278,757 +53375,273 @@ var init_coalesce_tasks = __esm({
53278
53375
  }
53279
53376
  });
53280
53377
 
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
- }));
53378
+ // src/runtime/concurrency.ts
53379
+ function defaultWorkflowConcurrency(workflowName, workflowMaxConcurrency) {
53380
+ if (workflowMaxConcurrency !== void 0) return workflowMaxConcurrency;
53381
+ if (workflowName === "parallel-research") return DEFAULT_CONCURRENCY.workflow.parallelResearch;
53382
+ if (workflowName === "research") return DEFAULT_CONCURRENCY.workflow.research;
53383
+ if (workflowName === "implementation") return DEFAULT_CONCURRENCY.workflow.implementation;
53384
+ if (workflowName === "review") return DEFAULT_CONCURRENCY.workflow.review;
53385
+ if (workflowName === "default") return DEFAULT_CONCURRENCY.workflow.default;
53386
+ return DEFAULT_CONCURRENCY.fallback;
53387
+ }
53388
+ function positiveInteger2(value) {
53389
+ if (value === void 0 || !Number.isFinite(value)) return void 0;
53390
+ return Math.max(1, Math.trunc(value));
53391
+ }
53392
+ function resolveBatchConcurrency(input) {
53393
+ const workflowMax = positiveInteger2(input.workflowMaxConcurrency);
53394
+ const defaultConcurrency = defaultWorkflowConcurrency(input.workflowName, workflowMax);
53395
+ const limitMax = positiveInteger2(input.limitMaxConcurrentWorkers);
53396
+ const teamMax = positiveInteger2(input.teamMaxConcurrency);
53397
+ const requested = limitMax ?? teamMax ?? workflowMax ?? defaultWorkflowConcurrency(input.workflowName);
53398
+ let source;
53399
+ if (limitMax !== void 0) source = "limit";
53400
+ else if (teamMax !== void 0) source = "team";
53401
+ else source = "workflow";
53402
+ const hardCap = positiveInteger2(input.hardCap) ?? DEFAULT_CONCURRENCY.hardCap;
53403
+ const maxConcurrent = input.allowUnboundedConcurrency ? requested : Math.min(requested, hardCap);
53404
+ const readyCount = Math.max(0, Math.trunc(Number.isFinite(input.readyCount) ? input.readyCount : 0));
53405
+ const cappedReason = maxConcurrent < requested ? `;capped:${hardCap}` : "";
53406
+ const unboundedReason = input.allowUnboundedConcurrency && requested > hardCap ? `;unbounded:${hardCap}` : "";
53407
+ return {
53408
+ maxConcurrent,
53409
+ selectedCount: readyCount === 0 ? 0 : Math.min(readyCount, maxConcurrent),
53410
+ defaultConcurrency,
53411
+ reason: `${source}:${requested}${cappedReason}${unboundedReason};ready:${readyCount}`
53412
+ };
53413
+ }
53414
+ var init_concurrency = __esm({
53415
+ "src/runtime/concurrency.ts"() {
53416
+ "use strict";
53417
+ init_defaults();
53305
53418
  }
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
- }
53419
+ });
53420
+
53421
+ // src/runtime/goal-achievement.ts
53422
+ import { spawnSync as spawnSync2 } from "node:child_process";
53423
+ function workflowIsMutating(workflow) {
53424
+ const steps = workflow?.steps ?? [];
53425
+ return steps.some((step) => typeof step?.role === "string" && MUTATING_ROLES2.has(step.role));
53426
+ }
53427
+ function isGitWorkingTreeClean(cwd) {
53428
+ try {
53429
+ const repoRoot = findRepoRoot(cwd);
53430
+ if (!repoRoot) return { clean: "unknown", repoRoot: void 0 };
53431
+ const res = spawnSync2("git", ["-C", repoRoot, "status", "--porcelain"], { encoding: "utf8", timeout: 5e3 });
53432
+ if (res.error || res.status !== 0) return { clean: "unknown", repoRoot: void 0 };
53433
+ return { clean: res.stdout.trim().length === 0, repoRoot };
53434
+ } catch {
53435
+ return { clean: "unknown", repoRoot: void 0 };
53315
53436
  }
53316
- return taskIds.map((id) => ({
53317
- taskId: id,
53318
- text: rawOutput,
53319
- strategy: "broadcast"
53320
- }));
53321
53437
  }
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 });
53438
+ function assessGoalAchievement(manifest, tasks, workflow) {
53439
+ if (!workflowIsMutating(workflow)) {
53440
+ return {
53441
+ achieved: "unknown",
53442
+ reason: "read-only / doc-only workflow (no executor/test-engineer steps)",
53443
+ signals: []
53444
+ };
53329
53445
  }
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;
53446
+ const tree = isGitWorkingTreeClean(manifest.cwd);
53447
+ if (tree.clean === "unknown" || !tree.repoRoot) {
53448
+ return {
53449
+ achieved: "unknown",
53450
+ reason: "not a git repo or git unavailable",
53451
+ signals: []
53452
+ };
53340
53453
  }
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
- }
53454
+ if (!tree.clean) {
53455
+ return {
53456
+ achieved: true,
53457
+ reason: "git working tree has changes (mutating run edited project files)",
53458
+ signals: [`repoRoot=${tree.repoRoot}`]
53459
+ };
53348
53460
  }
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
- }
53461
+ const failedTask = tasks.find((t2) => t2.status === "failed");
53462
+ const signals = ["git working tree clean despite mutating workflow"];
53463
+ if (failedTask) signals.push(`corroborating failed task: ${failedTask.id} (${failedTask.role})`);
53464
+ return {
53465
+ // `false` = false-green detected. Whether to downgrade status is decided
53466
+ // by the caller based on the corroborating failed-task signal.
53467
+ achieved: false,
53468
+ reason: "code-mutating run completed but made no project edits (false-green)",
53469
+ signals
53470
+ };
53471
+ }
53472
+ function applyGoalAchievement(manifest, assessment) {
53473
+ const note2 = assessment.achieved === false ? `goal-achievement: FALSE-GREEN \u2014 ${assessment.reason}. ${assessment.signals.join("; ")}` : assessment.achieved === true ? `goal-achievement: OK \u2014 ${assessment.reason}` : `goal-achievement: unknown \u2014 ${assessment.reason}`;
53474
+ const updated = {
53475
+ ...manifest,
53476
+ goalAchieved: assessment.achieved,
53477
+ goalAchievementNote: note2
53478
+ };
53479
+ const hasCorroboratingFailure = assessment.signals.some((s) => s.startsWith("corroborating failed task"));
53480
+ if (assessment.achieved === false && hasCorroboratingFailure && updated.status === "completed") {
53481
+ return { manifest: { ...updated, status: "failed" }, downgraded: true };
53365
53482
  }
53366
- return result4;
53483
+ return { manifest: updated, downgraded: false };
53367
53484
  }
53368
- var init_output_splitter = __esm({
53369
- "src/runtime/task-runner/output-splitter.ts"() {
53485
+ var MUTATING_ROLES2;
53486
+ var init_goal_achievement = __esm({
53487
+ "src/runtime/goal-achievement.ts"() {
53370
53488
  "use strict";
53489
+ init_paths();
53490
+ MUTATING_ROLES2 = /* @__PURE__ */ new Set(["executor", "test-engineer"]);
53371
53491
  }
53372
53492
  });
53373
53493
 
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`;
53494
+ // src/runtime/tool-output-pruner.ts
53495
+ function estimateTokens(text) {
53496
+ return Math.ceil(text.length / 4);
53382
53497
  }
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`;
53498
+ function firstErrorLine(text) {
53499
+ return text.split(/\r?\n/).find((line4) => /error|failed|exception|panic/i.test(line4))?.trim();
53389
53500
  }
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);
53501
+ function truncateField(value, maxLength) {
53502
+ if (value.length <= maxLength) return value;
53503
+ if (maxLength <= 1) return "\u2026";
53504
+ return `${value.slice(0, maxLength - 1)}\u2026`;
53394
53505
  }
53395
- function applyDirLimit(children, limit) {
53396
- if (children.length <= limit) {
53397
- return { visible: children, dropped: 0 };
53506
+ function resultDigest(toolName, content, isError) {
53507
+ const name = toolName.toLowerCase();
53508
+ const text = content ?? "";
53509
+ if (name === "bash") {
53510
+ const exitCode = isError ? 1 : 0;
53511
+ const tail = text.trim().split(/\r?\n/).filter(Boolean).at(-1) ?? "";
53512
+ const error = firstErrorLine(text);
53513
+ return [`exit=${exitCode}`, tail ? `tail=${tail}` : void 0, error ? `error=${error}` : void 0].filter((part) => part !== void 0).join("; ");
53398
53514
  }
53399
- if (limit <= 1) {
53400
- return {
53401
- visible: children.slice(0, limit),
53402
- dropped: children.length - limit
53403
- };
53515
+ if (name === "search" || name === "grep") {
53516
+ const match = text.match(/(\d+)\s+matches?/i) ?? text.match(/totalMatches["']?:\s*(\d+)/i);
53517
+ const files = text.match(/(\d+)\s+files?/i) ?? text.match(/filesWithMatches["']?:\s*(\d+)/i);
53518
+ const error = firstErrorLine(text);
53519
+ return [match ? `matches=${match[1]}` : void 0, files ? `files=${files[1]}` : void 0, error ? `error=${error}` : void 0].filter((part) => part !== void 0).join("; ") || "search digest unavailable";
53404
53520
  }
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 };
53521
+ return void 0;
53409
53522
  }
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 [];
53523
+ function createPrunedNotice(tokens, entry) {
53524
+ const generic = `[Output pruned \u2014 ${tokens} tokens]`;
53525
+ const digest = resultDigest(entry.toolName, entry.content, entry.isError);
53526
+ if (!digest) return generic;
53527
+ const genericTokens = Math.ceil(generic.length / 4);
53528
+ const maxTokens = Math.max(genericTokens, Math.floor(genericTokens * DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER));
53529
+ const prefix = `[Output pruned \u2014 ${tokens} tokens; `;
53530
+ const suffix = "]";
53531
+ const maxChars = Math.max(0, maxTokens * 4 - prefix.length - suffix.length);
53532
+ return `${prefix}${truncateField(digest, maxChars)}${suffix}`;
53533
+ }
53534
+ function readBasePath(filePath) {
53535
+ let base = filePath;
53536
+ while (READ_SELECTOR_SUFFIX.test(base)) {
53537
+ base = base.replace(READ_SELECTOR_SUFFIX, "");
53417
53538
  }
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);
53539
+ return base;
53442
53540
  }
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);
53541
+ function toolTargetKey(entry) {
53542
+ if (!entry.target || entry.target.length === 0) return void 0;
53543
+ return JSON.stringify([entry.toolName, entry.target]);
53544
+ }
53545
+ function buildStalenessIndex(toolResults, fileEdits = []) {
53546
+ const lastResultIndexByKey = /* @__PURE__ */ new Map();
53547
+ for (let i2 = 0; i2 < toolResults.length; i2++) {
53548
+ const entry = toolResults[i2];
53549
+ if (entry.isError) continue;
53550
+ const key = toolTargetKey(entry);
53551
+ if (key !== void 0) lastResultIndexByKey.set(key, i2);
53552
+ }
53553
+ const lastEditIndexByPath = /* @__PURE__ */ new Map();
53554
+ for (const edit of fileEdits) {
53555
+ lastEditIndexByPath.set(edit.target, edit.index);
53556
+ }
53557
+ const staleIndices = /* @__PURE__ */ new Set();
53558
+ for (let i2 = 0; i2 < toolResults.length; i2++) {
53559
+ const entry = toolResults[i2];
53560
+ const key = toolTargetKey(entry);
53561
+ if (key !== void 0) {
53562
+ const lastIndex = lastResultIndexByKey.get(key);
53563
+ if (lastIndex !== void 0 && lastIndex > i2) {
53564
+ staleIndices.add(i2);
53565
+ continue;
53566
+ }
53567
+ }
53568
+ if (entry.toolName.toLowerCase() === "read" && entry.target) {
53569
+ const basePath = readBasePath(entry.target);
53570
+ const editIndex = lastEditIndexByPath.get(basePath);
53571
+ if (editIndex !== void 0 && editIndex > i2) {
53572
+ staleIndices.add(i2);
53573
+ }
53469
53574
  }
53470
53575
  }
53471
- return { root, truncated };
53576
+ return { staleIndices };
53472
53577
  }
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
- });
53578
+ function pruneToolOutputs(results, config = DEFAULT_PRUNE_CONFIG) {
53579
+ const { staleIndices } = buildStalenessIndex(results);
53580
+ const staleOverridable = new Set(config.staleOverridableTools ?? []);
53581
+ let accumulatedTokens = 0;
53582
+ let tokensSaved = 0;
53583
+ let prunedCount = 0;
53584
+ const candidates = [];
53585
+ const prunedIds = [];
53586
+ for (let i2 = results.length - 1; i2 >= 0; i2--) {
53587
+ const entry = results[i2];
53588
+ const tokens = estimateTokens(entry.content);
53589
+ const isStale = staleIndices.has(i2);
53590
+ const isProtected = config.protectedTools.includes(entry.toolName) && !(isStale && staleOverridable.has(entry.toolName));
53591
+ const insideProtectWindow = accumulatedTokens < config.protectTokens;
53592
+ if (insideProtectWindow && !isStale || isProtected) {
53593
+ accumulatedTokens += tokens;
53594
+ continue;
53491
53595
  }
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
53596
+ const notice = createPrunedNotice(tokens, entry);
53597
+ candidates.push({
53598
+ index: i2,
53599
+ entry,
53600
+ tokens,
53601
+ notice,
53602
+ savings: Math.max(0, tokens - Math.ceil(notice.length / 4))
53503
53603
  });
53504
- if (oldestChild) collectLines(oldestChild, nowMs3, lines);
53505
- } else {
53506
- for (const child of node.children) collectLines(child, nowMs3, lines);
53604
+ accumulatedTokens += tokens;
53507
53605
  }
53606
+ for (const candidate of candidates) {
53607
+ tokensSaved += candidate.savings;
53608
+ }
53609
+ if (tokensSaved < config.minimumSavings || candidates.length === 0) {
53610
+ return { prunedCount: 0, tokensSaved: 0, results, prunedIds: [] };
53611
+ }
53612
+ const prunedResults = [...results];
53613
+ for (const candidate of candidates) {
53614
+ prunedResults[candidate.index] = {
53615
+ ...candidate.entry,
53616
+ content: candidate.notice
53617
+ };
53618
+ prunedIds.push(candidate.entry.id);
53619
+ prunedCount++;
53620
+ }
53621
+ return { prunedCount, tokensSaved, results: prunedResults, prunedIds };
53508
53622
  }
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;
53623
+ var DEFAULT_PRUNE_CONFIG, DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER, READ_SELECTOR_SUFFIX;
53624
+ var init_tool_output_pruner = __esm({
53625
+ "src/runtime/tool-output-pruner.ts"() {
53626
+ "use strict";
53627
+ DEFAULT_PRUNE_CONFIG = {
53628
+ protectTokens: 4e4,
53629
+ minimumSavings: 2e4,
53630
+ protectedTools: ["read"],
53631
+ staleOverridableTools: ["read"]
53632
+ };
53633
+ DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER = 1.25;
53634
+ READ_SELECTOR_SUFFIX = /:(?:raw|conflicts|\d+(?:[-+]\d+)?(?:,\d+(?:[-+]\d+)?)*)$/;
53533
53635
  }
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
- // src/runtime/concurrency.ts
53766
- function defaultWorkflowConcurrency(workflowName, workflowMaxConcurrency) {
53767
- if (workflowMaxConcurrency !== void 0) return workflowMaxConcurrency;
53768
- if (workflowName === "parallel-research") return DEFAULT_CONCURRENCY.workflow.parallelResearch;
53769
- if (workflowName === "research") return DEFAULT_CONCURRENCY.workflow.research;
53770
- if (workflowName === "implementation") return DEFAULT_CONCURRENCY.workflow.implementation;
53771
- if (workflowName === "review") return DEFAULT_CONCURRENCY.workflow.review;
53772
- if (workflowName === "default") return DEFAULT_CONCURRENCY.workflow.default;
53773
- return DEFAULT_CONCURRENCY.fallback;
53774
- }
53775
- function positiveInteger2(value) {
53776
- if (value === void 0 || !Number.isFinite(value)) return void 0;
53777
- return Math.max(1, Math.trunc(value));
53778
- }
53779
- function resolveBatchConcurrency(input) {
53780
- const workflowMax = positiveInteger2(input.workflowMaxConcurrency);
53781
- const defaultConcurrency = defaultWorkflowConcurrency(input.workflowName, workflowMax);
53782
- const limitMax = positiveInteger2(input.limitMaxConcurrentWorkers);
53783
- const teamMax = positiveInteger2(input.teamMaxConcurrency);
53784
- const requested = limitMax ?? teamMax ?? workflowMax ?? defaultWorkflowConcurrency(input.workflowName);
53785
- let source;
53786
- if (limitMax !== void 0) source = "limit";
53787
- else if (teamMax !== void 0) source = "team";
53788
- else source = "workflow";
53789
- const hardCap = positiveInteger2(input.hardCap) ?? DEFAULT_CONCURRENCY.hardCap;
53790
- const maxConcurrent = input.allowUnboundedConcurrency ? requested : Math.min(requested, hardCap);
53791
- const readyCount = Math.max(0, Math.trunc(Number.isFinite(input.readyCount) ? input.readyCount : 0));
53792
- const cappedReason = maxConcurrent < requested ? `;capped:${hardCap}` : "";
53793
- const unboundedReason = input.allowUnboundedConcurrency && requested > hardCap ? `;unbounded:${hardCap}` : "";
53794
- return {
53795
- maxConcurrent,
53796
- selectedCount: readyCount === 0 ? 0 : Math.min(readyCount, maxConcurrent),
53797
- defaultConcurrency,
53798
- reason: `${source}:${requested}${cappedReason}${unboundedReason};ready:${readyCount}`
53799
- };
53800
- }
53801
- var init_concurrency = __esm({
53802
- "src/runtime/concurrency.ts"() {
53803
- "use strict";
53804
- init_defaults();
53805
- }
53806
- });
53807
-
53808
- // src/runtime/goal-achievement.ts
53809
- import { spawnSync as spawnSync2 } from "node:child_process";
53810
- function workflowIsMutating(workflow) {
53811
- const steps = workflow?.steps ?? [];
53812
- return steps.some((step) => typeof step?.role === "string" && MUTATING_ROLES2.has(step.role));
53813
- }
53814
- function isGitWorkingTreeClean(cwd) {
53815
- try {
53816
- const repoRoot = findRepoRoot(cwd);
53817
- if (!repoRoot) return { clean: "unknown", repoRoot: void 0 };
53818
- const res = spawnSync2("git", ["-C", repoRoot, "status", "--porcelain"], { encoding: "utf8", timeout: 5e3 });
53819
- if (res.error || res.status !== 0) return { clean: "unknown", repoRoot: void 0 };
53820
- return { clean: res.stdout.trim().length === 0, repoRoot };
53821
- } catch {
53822
- return { clean: "unknown", repoRoot: void 0 };
53823
- }
53824
- }
53825
- function assessGoalAchievement(manifest, tasks, workflow) {
53826
- if (!workflowIsMutating(workflow)) {
53827
- return {
53828
- achieved: "unknown",
53829
- reason: "read-only / doc-only workflow (no executor/test-engineer steps)",
53830
- signals: []
53831
- };
53832
- }
53833
- const tree = isGitWorkingTreeClean(manifest.cwd);
53834
- if (tree.clean === "unknown" || !tree.repoRoot) {
53835
- return {
53836
- achieved: "unknown",
53837
- reason: "not a git repo or git unavailable",
53838
- signals: []
53839
- };
53840
- }
53841
- if (!tree.clean) {
53842
- return {
53843
- achieved: true,
53844
- reason: "git working tree has changes (mutating run edited project files)",
53845
- signals: [`repoRoot=${tree.repoRoot}`]
53846
- };
53847
- }
53848
- const failedTask = tasks.find((t2) => t2.status === "failed");
53849
- const signals = ["git working tree clean despite mutating workflow"];
53850
- if (failedTask) signals.push(`corroborating failed task: ${failedTask.id} (${failedTask.role})`);
53851
- return {
53852
- // `false` = false-green detected. Whether to downgrade status is decided
53853
- // by the caller based on the corroborating failed-task signal.
53854
- achieved: false,
53855
- reason: "code-mutating run completed but made no project edits (false-green)",
53856
- signals
53857
- };
53858
- }
53859
- function applyGoalAchievement(manifest, assessment) {
53860
- const note2 = assessment.achieved === false ? `goal-achievement: FALSE-GREEN \u2014 ${assessment.reason}. ${assessment.signals.join("; ")}` : assessment.achieved === true ? `goal-achievement: OK \u2014 ${assessment.reason}` : `goal-achievement: unknown \u2014 ${assessment.reason}`;
53861
- const updated = {
53862
- ...manifest,
53863
- goalAchieved: assessment.achieved,
53864
- goalAchievementNote: note2
53865
- };
53866
- const hasCorroboratingFailure = assessment.signals.some((s) => s.startsWith("corroborating failed task"));
53867
- if (assessment.achieved === false && hasCorroboratingFailure && updated.status === "completed") {
53868
- return { manifest: { ...updated, status: "failed" }, downgraded: true };
53869
- }
53870
- return { manifest: updated, downgraded: false };
53871
- }
53872
- var MUTATING_ROLES2;
53873
- var init_goal_achievement = __esm({
53874
- "src/runtime/goal-achievement.ts"() {
53875
- "use strict";
53876
- init_paths();
53877
- MUTATING_ROLES2 = /* @__PURE__ */ new Set(["executor", "test-engineer"]);
53878
- }
53879
- });
53880
-
53881
- // src/runtime/tool-output-pruner.ts
53882
- function estimateTokens(text) {
53883
- return Math.ceil(text.length / 4);
53884
- }
53885
- function firstErrorLine(text) {
53886
- return text.split(/\r?\n/).find((line4) => /error|failed|exception|panic/i.test(line4))?.trim();
53887
- }
53888
- function truncateField(value, maxLength) {
53889
- if (value.length <= maxLength) return value;
53890
- if (maxLength <= 1) return "\u2026";
53891
- return `${value.slice(0, maxLength - 1)}\u2026`;
53892
- }
53893
- function resultDigest(toolName, content, isError) {
53894
- const name = toolName.toLowerCase();
53895
- const text = content ?? "";
53896
- if (name === "bash") {
53897
- const exitCode = isError ? 1 : 0;
53898
- const tail = text.trim().split(/\r?\n/).filter(Boolean).at(-1) ?? "";
53899
- const error = firstErrorLine(text);
53900
- return [`exit=${exitCode}`, tail ? `tail=${tail}` : void 0, error ? `error=${error}` : void 0].filter((part) => part !== void 0).join("; ");
53901
- }
53902
- if (name === "search" || name === "grep") {
53903
- const match = text.match(/(\d+)\s+matches?/i) ?? text.match(/totalMatches["']?:\s*(\d+)/i);
53904
- const files = text.match(/(\d+)\s+files?/i) ?? text.match(/filesWithMatches["']?:\s*(\d+)/i);
53905
- const error = firstErrorLine(text);
53906
- return [match ? `matches=${match[1]}` : void 0, files ? `files=${files[1]}` : void 0, error ? `error=${error}` : void 0].filter((part) => part !== void 0).join("; ") || "search digest unavailable";
53907
- }
53908
- return void 0;
53909
- }
53910
- function createPrunedNotice(tokens, entry) {
53911
- const generic = `[Output pruned \u2014 ${tokens} tokens]`;
53912
- const digest = resultDigest(entry.toolName, entry.content, entry.isError);
53913
- if (!digest) return generic;
53914
- const genericTokens = Math.ceil(generic.length / 4);
53915
- const maxTokens = Math.max(genericTokens, Math.floor(genericTokens * DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER));
53916
- const prefix = `[Output pruned \u2014 ${tokens} tokens; `;
53917
- const suffix = "]";
53918
- const maxChars = Math.max(0, maxTokens * 4 - prefix.length - suffix.length);
53919
- return `${prefix}${truncateField(digest, maxChars)}${suffix}`;
53920
- }
53921
- function readBasePath(filePath) {
53922
- let base = filePath;
53923
- while (READ_SELECTOR_SUFFIX.test(base)) {
53924
- base = base.replace(READ_SELECTOR_SUFFIX, "");
53925
- }
53926
- return base;
53927
- }
53928
- function toolTargetKey(entry) {
53929
- if (!entry.target || entry.target.length === 0) return void 0;
53930
- return JSON.stringify([entry.toolName, entry.target]);
53931
- }
53932
- function buildStalenessIndex(toolResults, fileEdits = []) {
53933
- const lastResultIndexByKey = /* @__PURE__ */ new Map();
53934
- for (let i2 = 0; i2 < toolResults.length; i2++) {
53935
- const entry = toolResults[i2];
53936
- if (entry.isError) continue;
53937
- const key = toolTargetKey(entry);
53938
- if (key !== void 0) lastResultIndexByKey.set(key, i2);
53939
- }
53940
- const lastEditIndexByPath = /* @__PURE__ */ new Map();
53941
- for (const edit of fileEdits) {
53942
- lastEditIndexByPath.set(edit.target, edit.index);
53943
- }
53944
- const staleIndices = /* @__PURE__ */ new Set();
53945
- for (let i2 = 0; i2 < toolResults.length; i2++) {
53946
- const entry = toolResults[i2];
53947
- const key = toolTargetKey(entry);
53948
- if (key !== void 0) {
53949
- const lastIndex = lastResultIndexByKey.get(key);
53950
- if (lastIndex !== void 0 && lastIndex > i2) {
53951
- staleIndices.add(i2);
53952
- continue;
53953
- }
53954
- }
53955
- if (entry.toolName.toLowerCase() === "read" && entry.target) {
53956
- const basePath = readBasePath(entry.target);
53957
- const editIndex = lastEditIndexByPath.get(basePath);
53958
- if (editIndex !== void 0 && editIndex > i2) {
53959
- staleIndices.add(i2);
53960
- }
53961
- }
53962
- }
53963
- return { staleIndices };
53964
- }
53965
- function pruneToolOutputs(results, config = DEFAULT_PRUNE_CONFIG) {
53966
- const { staleIndices } = buildStalenessIndex(results);
53967
- const staleOverridable = new Set(config.staleOverridableTools ?? []);
53968
- let accumulatedTokens = 0;
53969
- let tokensSaved = 0;
53970
- let prunedCount = 0;
53971
- const candidates = [];
53972
- const prunedIds = [];
53973
- for (let i2 = results.length - 1; i2 >= 0; i2--) {
53974
- const entry = results[i2];
53975
- const tokens = estimateTokens(entry.content);
53976
- const isStale = staleIndices.has(i2);
53977
- const isProtected = config.protectedTools.includes(entry.toolName) && !(isStale && staleOverridable.has(entry.toolName));
53978
- const insideProtectWindow = accumulatedTokens < config.protectTokens;
53979
- if (insideProtectWindow && !isStale || isProtected) {
53980
- accumulatedTokens += tokens;
53981
- continue;
53982
- }
53983
- const notice = createPrunedNotice(tokens, entry);
53984
- candidates.push({
53985
- index: i2,
53986
- entry,
53987
- tokens,
53988
- notice,
53989
- savings: Math.max(0, tokens - Math.ceil(notice.length / 4))
53990
- });
53991
- accumulatedTokens += tokens;
53992
- }
53993
- for (const candidate of candidates) {
53994
- tokensSaved += candidate.savings;
53995
- }
53996
- if (tokensSaved < config.minimumSavings || candidates.length === 0) {
53997
- return { prunedCount: 0, tokensSaved: 0, results, prunedIds: [] };
53998
- }
53999
- const prunedResults = [...results];
54000
- for (const candidate of candidates) {
54001
- prunedResults[candidate.index] = {
54002
- ...candidate.entry,
54003
- content: candidate.notice
54004
- };
54005
- prunedIds.push(candidate.entry.id);
54006
- prunedCount++;
54007
- }
54008
- return { prunedCount, tokensSaved, results: prunedResults, prunedIds };
54009
- }
54010
- var DEFAULT_PRUNE_CONFIG, DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER, READ_SELECTOR_SUFFIX;
54011
- var init_tool_output_pruner = __esm({
54012
- "src/runtime/tool-output-pruner.ts"() {
54013
- "use strict";
54014
- DEFAULT_PRUNE_CONFIG = {
54015
- protectTokens: 4e4,
54016
- minimumSavings: 2e4,
54017
- protectedTools: ["read"],
54018
- staleOverridableTools: ["read"]
54019
- };
54020
- DIGEST_NOTICE_TOKEN_CAP_MULTIPLIER = 1.25;
54021
- READ_SELECTOR_SUFFIX = /:(?:raw|conflicts|\d+(?:[-+]\d+)?(?:,\d+(?:[-+]\d+)?)*)$/;
54022
- }
54023
- });
54024
-
54025
- // src/runtime/task-output-context.ts
54026
- import * as fs74 from "node:fs";
54027
- import * as path62 from "node:path";
54028
- function containedExists(filePath, baseDir) {
53636
+ });
53637
+
53638
+ // src/runtime/task-output-context.ts
53639
+ import * as fs73 from "node:fs";
53640
+ import * as path61 from "node:path";
53641
+ function containedExists(filePath, baseDir) {
54029
53642
  try {
54030
53643
  const safePath = baseDir ? resolveRealContainedPath(baseDir, filePath) : filePath;
54031
- return fs74.existsSync(safePath);
53644
+ return fs73.existsSync(safePath);
54032
53645
  } catch {
54033
53646
  return false;
54034
53647
  }
@@ -54038,12 +53651,12 @@ function safeTeeName(taskId, artifactName) {
54038
53651
  return `${safe(taskId)}-${safe(artifactName)}.full.txt`;
54039
53652
  }
54040
53653
  function teePathForArtifact(artifactsRoot, taskId, artifactName) {
54041
- return path62.join(artifactsRoot, "tee", safeTeeName(taskId, artifactName));
53654
+ return path61.join(artifactsRoot, "tee", safeTeeName(taskId, artifactName));
54042
53655
  }
54043
53656
  function writeTeeFile(fullOutputPath, content) {
54044
53657
  try {
54045
- fs74.mkdirSync(path62.dirname(fullOutputPath), { recursive: true });
54046
- fs74.writeFileSync(fullOutputPath, content, "utf-8");
53658
+ fs73.mkdirSync(path61.dirname(fullOutputPath), { recursive: true });
53659
+ fs73.writeFileSync(fullOutputPath, content, "utf-8");
54047
53660
  return true;
54048
53661
  } catch {
54049
53662
  return false;
@@ -54053,7 +53666,7 @@ function readIfSmallWithTee(filePath, opts = {}) {
54053
53666
  const maxChars = MAX_RESULT_INLINE_BYTES;
54054
53667
  try {
54055
53668
  const safePath = opts.baseDir ? resolveRealContainedPath(opts.baseDir, filePath) : filePath;
54056
- const content = fs74.readFileSync(safePath, "utf-8");
53669
+ const content = fs73.readFileSync(safePath, "utf-8");
54057
53670
  if (content.length > maxChars) {
54058
53671
  let fullOutputPath;
54059
53672
  if (opts.tee && content.length > maxChars * TEE_THRESHOLD_MULTIPLIER) {
@@ -54087,15 +53700,15 @@ function readIfSmall(filePath, baseDir) {
54087
53700
  }
54088
53701
  function safeSharedName(name) {
54089
53702
  const normalized = name.replaceAll("\\", "/").replace(/^\.\/+/, "");
54090
- if (!normalized || normalized.split("/").some((segment) => segment === "..") || path62.isAbsolute(normalized))
53703
+ if (!normalized || normalized.split("/").some((segment) => segment === "..") || path61.isAbsolute(normalized))
54091
53704
  throw new Error(`Invalid shared artifact name: ${name}`);
54092
53705
  return normalized;
54093
53706
  }
54094
53707
  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}`);
53708
+ const sharedRoot = path61.resolve(manifest.artifactsRoot, "shared");
53709
+ const resolved = path61.resolve(sharedRoot, safeSharedName(name));
53710
+ const relative8 = path61.relative(sharedRoot, resolved);
53711
+ if (relative8.startsWith("..") || path61.isAbsolute(relative8)) throw new Error(`Invalid shared artifact name: ${name}`);
54099
53712
  return resolved;
54100
53713
  }
54101
53714
  function tryParseJson(text) {
@@ -54110,7 +53723,7 @@ function listTaskArtifacts(manifest, taskId) {
54110
53723
  const produced = manifest.artifacts.filter((a) => a.producer === taskId);
54111
53724
  if (produced.length === 0) return void 0;
54112
53725
  return produced.map((a) => {
54113
- const relative8 = path62.relative(manifest.artifactsRoot, a.path);
53726
+ const relative8 = path61.relative(manifest.artifactsRoot, a.path);
54114
53727
  return relative8.startsWith("..") ? a.path : relative8;
54115
53728
  });
54116
53729
  }
@@ -54168,7 +53781,7 @@ function pruneSharedReads(reads, dependencies, artifactsRoot) {
54168
53781
  for (const artifact of produced) {
54169
53782
  if (typeof artifact !== "string") continue;
54170
53783
  fileEdits.push({
54171
- target: path62.resolve(artifactsRoot, artifact),
53784
+ target: path61.resolve(artifactsRoot, artifact),
54172
53785
  index: reads.length + depIndex
54173
53786
  });
54174
53787
  }
@@ -54221,7 +53834,7 @@ function collectDependencyOutputContext(manifest, tasks, task, step) {
54221
53834
  const filePath = sharedPath(manifest, name);
54222
53835
  const teePath = teePathForArtifact(manifest.artifactsRoot, task.id, name);
54223
53836
  const teeResult = readIfSmallWithTee(filePath, {
54224
- baseDir: path62.resolve(manifest.artifactsRoot, "shared"),
53837
+ baseDir: path61.resolve(manifest.artifactsRoot, "shared"),
54225
53838
  tee: { fullOutputPath: teePath }
54226
53839
  });
54227
53840
  if (teeResult === void 0) return { name, path: filePath, content: "" };
@@ -54733,6 +54346,502 @@ var init_retry_executor = __esm({
54733
54346
  }
54734
54347
  });
54735
54348
 
54349
+ // src/runtime/task-runner/output-splitter.ts
54350
+ function splitCoalescedOutput(rawOutput, taskIds) {
54351
+ if (taskIds.length === 0) return [];
54352
+ if (taskIds.length === 1) {
54353
+ return [{ taskId: taskIds[0], text: rawOutput, strategy: "delimiter" }];
54354
+ }
54355
+ const byId = /* @__PURE__ */ new Map();
54356
+ const delimiterHits = /* @__PURE__ */ new Set();
54357
+ const delimiterRegex = /<<<TASK_RESULT:([^\s>]+)>>>([\s\S]*?)<<<END_TASK_RESULT>>>/g;
54358
+ let match;
54359
+ while ((match = delimiterRegex.exec(rawOutput)) !== null) {
54360
+ const id = match[1];
54361
+ const body = match[2].trim();
54362
+ if (taskIds.includes(id) && !byId.has(id)) {
54363
+ byId.set(id, body);
54364
+ delimiterHits.add(id);
54365
+ }
54366
+ }
54367
+ if (delimiterHits.size === taskIds.length) {
54368
+ return taskIds.map((id) => ({
54369
+ taskId: id,
54370
+ text: byId.get(id) ?? "",
54371
+ strategy: "delimiter"
54372
+ }));
54373
+ }
54374
+ if (delimiterHits.size === 0) {
54375
+ const bySection = parseBySectionHeadings(rawOutput, taskIds);
54376
+ if (bySection.size === taskIds.length) {
54377
+ return taskIds.map((id) => ({
54378
+ taskId: id,
54379
+ text: bySection.get(id) ?? "",
54380
+ strategy: "section"
54381
+ }));
54382
+ }
54383
+ }
54384
+ return taskIds.map((id) => ({
54385
+ taskId: id,
54386
+ text: rawOutput,
54387
+ strategy: "broadcast"
54388
+ }));
54389
+ }
54390
+ function parseBySectionHeadings(rawOutput, taskIds) {
54391
+ const result4 = /* @__PURE__ */ new Map();
54392
+ const numberedRegex = /^(#{2,3})\s+Task\s+(\d+)\s+of\s+\d+.*$/gm;
54393
+ const sections = [];
54394
+ let m;
54395
+ while ((m = numberedRegex.exec(rawOutput)) !== null) {
54396
+ sections.push({ num: Number(m[2]), start: m.index + m[0].length });
54397
+ }
54398
+ if (sections.length >= taskIds.length) {
54399
+ sections.sort((a, b) => a.start - b.start);
54400
+ for (let i2 = 0; i2 < sections.length && i2 < taskIds.length; i2 += 1) {
54401
+ const section2 = sections[i2];
54402
+ const end = sections[i2 + 1]?.start ?? rawOutput.length;
54403
+ const text = rawOutput.slice(section2.start, end).trim();
54404
+ const taskId = taskIds[i2];
54405
+ if (taskId) result4.set(taskId, text);
54406
+ }
54407
+ return result4;
54408
+ }
54409
+ const idHeaderRegex = /^(#{2,3})\s+Task\s+([^\s].*?)$/gm;
54410
+ while ((m = idHeaderRegex.exec(rawOutput)) !== null) {
54411
+ const headerText = m[2].trim();
54412
+ const matchedId = taskIds.find((id) => headerText.includes(id));
54413
+ if (matchedId && !result4.has(matchedId)) {
54414
+ result4.set(matchedId, "");
54415
+ }
54416
+ }
54417
+ if (result4.size === taskIds.length) {
54418
+ const headerPositions = [];
54419
+ const idHeaderRegex2 = /^(#{2,3})\s+Task\s+([^\s].*?)$/gm;
54420
+ while ((m = idHeaderRegex2.exec(rawOutput)) !== null) {
54421
+ const headerText = m[2].trim();
54422
+ const matchedId = taskIds.find((id) => headerText.includes(id));
54423
+ if (matchedId) headerPositions.push({ taskId: matchedId, start: m.index + m[0].length });
54424
+ }
54425
+ headerPositions.sort((a, b) => a.start - b.start);
54426
+ for (let i2 = 0; i2 < headerPositions.length; i2 += 1) {
54427
+ const cur = headerPositions[i2];
54428
+ const next = headerPositions[i2 + 1];
54429
+ const end = next ? rawOutput.lastIndexOf("\n#", next.start) : rawOutput.length;
54430
+ const text = rawOutput.slice(cur.start, end > 0 ? end : rawOutput.length).trim();
54431
+ result4.set(cur.taskId, text);
54432
+ }
54433
+ }
54434
+ return result4;
54435
+ }
54436
+ var init_output_splitter = __esm({
54437
+ "src/runtime/task-runner/output-splitter.ts"() {
54438
+ "use strict";
54439
+ }
54440
+ });
54441
+
54442
+ // src/runtime/team-runner-artifacts.ts
54443
+ function mergeArtifacts(items) {
54444
+ const byPath = /* @__PURE__ */ new Map();
54445
+ for (const item of items) byPath.set(item.path, item);
54446
+ return [...byPath.values()];
54447
+ }
54448
+ var init_team_runner_artifacts = __esm({
54449
+ "src/runtime/team-runner-artifacts.ts"() {
54450
+ "use strict";
54451
+ }
54452
+ });
54453
+
54454
+ // src/runtime/workspace-tree.ts
54455
+ import * as fs74 from "node:fs/promises";
54456
+ import * as path62 from "node:path";
54457
+ function formatBytes2(bytes) {
54458
+ if (bytes < 1024) return `${bytes}B`;
54459
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
54460
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
54461
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
54462
+ }
54463
+ function formatAge(seconds2) {
54464
+ if (seconds2 < 60) return "just now";
54465
+ if (seconds2 < 3600) return `${Math.floor(seconds2 / 60)}m`;
54466
+ if (seconds2 < 86400) return `${Math.floor(seconds2 / 3600)}h`;
54467
+ if (seconds2 < 86400 * 14) return `${Math.floor(seconds2 / 86400)}d`;
54468
+ return `${Math.floor(seconds2 / (86400 * 7))}w`;
54469
+ }
54470
+ function compareByRecency(a, b) {
54471
+ const diff = b.mtimeMs - a.mtimeMs;
54472
+ if (diff !== 0) return diff;
54473
+ return a.name.localeCompare(b.name);
54474
+ }
54475
+ function applyDirLimit(children, limit) {
54476
+ if (children.length <= limit) {
54477
+ return { visible: children, dropped: 0 };
54478
+ }
54479
+ if (limit <= 1) {
54480
+ return {
54481
+ visible: children.slice(0, limit),
54482
+ dropped: children.length - limit
54483
+ };
54484
+ }
54485
+ const recent = children.slice(0, limit - 1);
54486
+ const oldest = children[children.length - 1];
54487
+ const visible = oldest ? [...recent, oldest] : recent;
54488
+ return { visible, dropped: children.length - limit };
54489
+ }
54490
+ async function readChildren(rootPath, parent, excludedDirs) {
54491
+ const dirPath = parent.relativePath ? path62.join(rootPath, parent.relativePath) : rootPath;
54492
+ let names;
54493
+ try {
54494
+ names = await fs74.readdir(dirPath);
54495
+ } catch {
54496
+ return [];
54497
+ }
54498
+ const nodes = await Promise.all(
54499
+ names.map(async (name) => {
54500
+ if (name.startsWith(".")) return null;
54501
+ const relativePath = parent.relativePath ? `${parent.relativePath}/${name}` : name;
54502
+ const absolutePath = path62.join(rootPath, relativePath);
54503
+ try {
54504
+ const stat2 = await fs74.stat(absolutePath);
54505
+ if (stat2.isDirectory() && excludedDirs.has(name)) return null;
54506
+ return {
54507
+ name,
54508
+ relativePath,
54509
+ depth: parent.depth + 1,
54510
+ isDirectory: stat2.isDirectory(),
54511
+ mtimeMs: stat2.mtimeMs,
54512
+ size: stat2.size,
54513
+ children: [],
54514
+ droppedChildCount: 0
54515
+ };
54516
+ } catch {
54517
+ return null;
54518
+ }
54519
+ })
54520
+ );
54521
+ return nodes.filter((n) => n !== null).sort(compareByRecency);
54522
+ }
54523
+ async function collectTree(rootPath, maxDepth, dirLimit, excludedDirs) {
54524
+ const rootStat = await fs74.stat(rootPath);
54525
+ const root = {
54526
+ name: ".",
54527
+ relativePath: "",
54528
+ depth: 0,
54529
+ isDirectory: true,
54530
+ mtimeMs: rootStat.mtimeMs,
54531
+ size: rootStat.size,
54532
+ children: [],
54533
+ droppedChildCount: 0
54534
+ };
54535
+ let truncated = false;
54536
+ const queue = [root];
54537
+ let cursor = 0;
54538
+ while (cursor < queue.length) {
54539
+ const parent = queue[cursor];
54540
+ cursor += 1;
54541
+ if (!parent || parent.depth >= maxDepth) continue;
54542
+ const children = await readChildren(rootPath, parent, excludedDirs);
54543
+ const { visible, dropped } = applyDirLimit(children, dirLimit);
54544
+ parent.children = visible;
54545
+ parent.droppedChildCount = dropped;
54546
+ if (dropped > 0) truncated = true;
54547
+ for (const child of visible) {
54548
+ if (child.isDirectory) queue.push(child);
54549
+ }
54550
+ }
54551
+ return { root, truncated };
54552
+ }
54553
+ function collectLines(node, nowMs3, lines) {
54554
+ if (node.depth === 0) {
54555
+ lines.push({ text: ".", depth: 0, isRoot: true });
54556
+ } else {
54557
+ const indent = " ".repeat(node.depth);
54558
+ const suffix = node.isDirectory ? "/" : "";
54559
+ const label = `${indent}- ${node.name}${suffix}`;
54560
+ if (node.isDirectory) {
54561
+ lines.push({ text: label, depth: node.depth, isRoot: false });
54562
+ } else {
54563
+ const ageSeconds = Math.max(0, Math.floor((nowMs3 - node.mtimeMs) / 1e3));
54564
+ const size = formatBytes2(node.size);
54565
+ const age = formatAge(ageSeconds);
54566
+ lines.push({
54567
+ text: `${label} ${size} ${age}`,
54568
+ depth: node.depth,
54569
+ isRoot: false
54570
+ });
54571
+ }
54572
+ }
54573
+ if (node.droppedChildCount > 0) {
54574
+ const recentChildren = node.children.slice(0, -1);
54575
+ const oldestChild = node.children[node.children.length - 1];
54576
+ for (const child of recentChildren) collectLines(child, nowMs3, lines);
54577
+ const childDepth = node.depth + 1;
54578
+ const indent = " ".repeat(childDepth);
54579
+ lines.push({
54580
+ text: `${indent}- \u2026 ${node.droppedChildCount} more`,
54581
+ depth: childDepth,
54582
+ isRoot: false
54583
+ });
54584
+ if (oldestChild) collectLines(oldestChild, nowMs3, lines);
54585
+ } else {
54586
+ for (const child of node.children) collectLines(child, nowMs3, lines);
54587
+ }
54588
+ }
54589
+ function applyLineCap(lines, cap) {
54590
+ if (lines.length <= cap) return { lines, elided: 0 };
54591
+ const target = Math.max(1, cap - 1);
54592
+ const removeCount = lines.length - target;
54593
+ 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);
54594
+ if (removable.length === 0) return { lines, elided: 0 };
54595
+ const removedIndexes = new Set(removable.map((item) => item.index));
54596
+ const kept = lines.filter((_, index) => !removedIndexes.has(index));
54597
+ kept.push({
54598
+ text: `\u2026 (${removable.length} lines elided)`,
54599
+ depth: 0,
54600
+ isRoot: false
54601
+ });
54602
+ return { lines: kept, elided: removable.length };
54603
+ }
54604
+ function treeCacheKey(cwd, options) {
54605
+ return `${path62.resolve(cwd)}|${options?.maxDepth ?? ""}|${options?.dirLimit ?? ""}|${options?.lineCap ?? ""}`;
54606
+ }
54607
+ async function buildWorkspaceTree(cwd, options) {
54608
+ const rootPath = path62.resolve(cwd);
54609
+ const cacheKey2 = treeCacheKey(cwd, options);
54610
+ const cached = treeCache.get(cacheKey2);
54611
+ if (cached && cached.expiresAt > Date.now()) {
54612
+ return cached.tree;
54613
+ }
54614
+ try {
54615
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
54616
+ const dirLimit = options?.dirLimit ?? DEFAULT_DIR_LIMIT;
54617
+ const lineCap = options?.lineCap ?? DEFAULT_LINE_CAP;
54618
+ const excludedDirs = options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS;
54619
+ const { root, truncated: dirTruncated } = await collectTree(rootPath, maxDepth, dirLimit, excludedDirs);
54620
+ const nowMs3 = Date.now();
54621
+ const lines = [];
54622
+ collectLines(root, nowMs3, lines);
54623
+ const { lines: capped, elided } = applyLineCap(lines, lineCap);
54624
+ const rendered = capped.map((l) => l.text).join("\n");
54625
+ const result4 = {
54626
+ rootPath,
54627
+ rendered,
54628
+ truncated: dirTruncated || elided > 0,
54629
+ totalLines: capped.length
54630
+ };
54631
+ treeCache.set(cacheKey2, {
54632
+ tree: result4,
54633
+ expiresAt: Date.now() + TREE_CACHE_TTL_MS
54634
+ });
54635
+ return result4;
54636
+ } catch {
54637
+ return emptyResult(rootPath);
54638
+ }
54639
+ }
54640
+ var DEFAULT_MAX_DEPTH, DEFAULT_DIR_LIMIT, DEFAULT_LINE_CAP, DEFAULT_EXCLUDED_DIRS, emptyResult, TREE_CACHE_TTL_MS, treeCache;
54641
+ var init_workspace_tree = __esm({
54642
+ "src/runtime/workspace-tree.ts"() {
54643
+ "use strict";
54644
+ DEFAULT_MAX_DEPTH = 3;
54645
+ DEFAULT_DIR_LIMIT = 12;
54646
+ DEFAULT_LINE_CAP = 120;
54647
+ DEFAULT_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
54648
+ "node_modules",
54649
+ ".git",
54650
+ ".next",
54651
+ "dist",
54652
+ "build",
54653
+ "target",
54654
+ ".venv",
54655
+ ".cache",
54656
+ ".turbo"
54657
+ ]);
54658
+ emptyResult = (rootPath) => ({
54659
+ rootPath,
54660
+ rendered: "",
54661
+ truncated: false,
54662
+ totalLines: 0
54663
+ });
54664
+ TREE_CACHE_TTL_MS = 3e4;
54665
+ treeCache = /* @__PURE__ */ new Map();
54666
+ }
54667
+ });
54668
+
54669
+ // src/runtime/run-coalesced-task-group.ts
54670
+ async function runCoalescedTaskGroup(input) {
54671
+ const { manifest, groupTasks, step, agent, signal, executeWorkers } = input;
54672
+ const groupId = groupTasks.map((t2) => t2.id).join("+");
54673
+ const firstTask = groupTasks[0];
54674
+ const taskIds = groupTasks.map((t2) => t2.id);
54675
+ let updatedTasks = input.tasks.map((t2) => {
54676
+ if (taskIds.includes(t2.id) && t2.status !== "running") {
54677
+ return { ...t2, status: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString() };
54678
+ }
54679
+ return t2;
54680
+ });
54681
+ saveRunTasks(manifest, updatedTasks);
54682
+ await appendEventAsync(manifest.eventsPath, {
54683
+ type: "task.coalesced_dispatch_start",
54684
+ runId: manifest.runId,
54685
+ message: `Dispatching ${groupTasks.length} coalesced tasks in 1 worker (role=${firstTask.role}, cwd=${firstTask.cwd})`,
54686
+ data: { groupId, role: firstTask.role, cwd: firstTask.cwd, taskIds }
54687
+ });
54688
+ const combinedPrompt = await buildCoalescedPrompt(manifest, step, groupTasks, agent);
54689
+ updatedTasks = updatedTasks.map((t2) => {
54690
+ if (!taskIds.includes(t2.id)) return t2;
54691
+ return {
54692
+ ...t2,
54693
+ heartbeat: t2.heartbeat ?? createWorkerHeartbeat(t2.id)
54694
+ };
54695
+ });
54696
+ saveRunTasks(manifest, updatedTasks);
54697
+ let rawOutput = "";
54698
+ let success = false;
54699
+ if (!executeWorkers) {
54700
+ rawOutput = buildScaffoldOutput(groupTasks);
54701
+ success = true;
54702
+ } else {
54703
+ const heartbeatTimer = setInterval(() => {
54704
+ const now = (/* @__PURE__ */ new Date()).toISOString();
54705
+ updatedTasks = updatedTasks.map((t2) => {
54706
+ if (!taskIds.includes(t2.id)) return t2;
54707
+ return {
54708
+ ...t2,
54709
+ heartbeat: touchWorkerHeartbeat(t2.heartbeat ?? createWorkerHeartbeat(t2.id), { alive: true })
54710
+ };
54711
+ });
54712
+ try {
54713
+ saveRunTasks(manifest, updatedTasks);
54714
+ } catch {
54715
+ }
54716
+ }, 15e3);
54717
+ try {
54718
+ const result4 = await runChildPi({
54719
+ cwd: firstTask.cwd,
54720
+ task: combinedPrompt,
54721
+ agent,
54722
+ signal,
54723
+ excludeContextBash: true,
54724
+ maxTurns: 5,
54725
+ onJsonEvent: (e) => input.onJsonEvent?.(firstTask.id, manifest.runId, e)
54726
+ });
54727
+ rawOutput = result4.rawFinalText ?? result4.stdout ?? "";
54728
+ success = result4.exitStatus?.exitCode === 0;
54729
+ } catch (err2) {
54730
+ rawOutput = `Worker dispatch failed: ${err2 instanceof Error ? err2.message : String(err2)}`;
54731
+ success = false;
54732
+ } finally {
54733
+ clearInterval(heartbeatTimer);
54734
+ }
54735
+ }
54736
+ const split = splitCoalescedOutput(rawOutput, taskIds);
54737
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
54738
+ const newArtifacts = [];
54739
+ updatedTasks = updatedTasks.map((t2) => {
54740
+ if (!taskIds.includes(t2.id)) return t2;
54741
+ const entry = split.find((s) => s.taskId === t2.id);
54742
+ const ok = success && Boolean(entry?.text);
54743
+ const text = entry?.text ?? rawOutput;
54744
+ const resultArtifact = writeArtifact(manifest.artifactsRoot, {
54745
+ kind: "result",
54746
+ relativePath: `results/${t2.id}.txt`,
54747
+ content: text,
54748
+ producer: t2.id
54749
+ });
54750
+ newArtifacts.push(resultArtifact);
54751
+ return {
54752
+ ...t2,
54753
+ status: ok ? "completed" : "failed",
54754
+ finishedAt,
54755
+ result: {
54756
+ text,
54757
+ producer: groupId,
54758
+ strategy: entry?.strategy ?? "broadcast"
54759
+ },
54760
+ resultArtifact
54761
+ };
54762
+ });
54763
+ saveRunTasks(manifest, updatedTasks);
54764
+ let updatedManifest = {
54765
+ ...manifest,
54766
+ artifacts: mergeArtifacts([...manifest.artifacts, ...newArtifacts])
54767
+ };
54768
+ if (success) {
54769
+ updatedManifest = updateRunStatus(updatedManifest, "running");
54770
+ }
54771
+ await appendEventAsync(updatedManifest.eventsPath, {
54772
+ type: "task.coalesced_dispatch_end",
54773
+ runId: manifest.runId,
54774
+ message: `Coalesced dispatch ${success ? "completed" : "failed"} (${taskIds.length} tasks, ${split[0]?.strategy ?? "broadcast"} split)`,
54775
+ data: { groupId, taskIds, success, strategy: split[0]?.strategy }
54776
+ });
54777
+ return { manifest: updatedManifest, tasks: updatedTasks, taskIds, rawOutput, success };
54778
+ }
54779
+ async function buildCoalescedPrompt(manifest, step, groupTasks, agent) {
54780
+ const tree = await buildWorkspaceTree(groupTasks[0].cwd);
54781
+ const treeBlock = tree.rendered ? `# Workspace Structure
54782
+ ${tree.rendered}` : "";
54783
+ 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.` : "";
54784
+ const taskBlocks = groupTasks.map((task, idx) => {
54785
+ return [
54786
+ `### Task ${idx + 1} of ${groupTasks.length} (id: ${task.id})`,
54787
+ `Step: ${step.id}`,
54788
+ `Role: ${step.role}`,
54789
+ `Task: ${step.task.replaceAll("{goal}", manifest.goal)}`
54790
+ ].join("\n");
54791
+ }).join("\n\n---\n\n");
54792
+ const outputInstructions = groupTasks.map((task) => `<<<TASK_RESULT:${task.id}>>>`).join(" ... ");
54793
+ return [
54794
+ "# pi-crew Coalesced Worker Prompt",
54795
+ `Run ID: ${manifest.runId}`,
54796
+ `Team: ${manifest.team}`,
54797
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
54798
+ `Goal: ${manifest.goal}`,
54799
+ `Tasks in this batch: ${groupTasks.length}`,
54800
+ ``,
54801
+ roleInstructions,
54802
+ ``,
54803
+ treeBlock,
54804
+ ``,
54805
+ `# Your Tasks`,
54806
+ `Complete ALL ${groupTasks.length} tasks below. For each, structure your final output using the delimiters shown.`,
54807
+ ``,
54808
+ taskBlocks,
54809
+ ``,
54810
+ `# Output Format (CRITICAL)`,
54811
+ `After completing all tasks, structure your final output using these delimiters:`,
54812
+ ``,
54813
+ outputInstructions,
54814
+ ``,
54815
+ `Wrap each task's result between the start and end delimiters:`,
54816
+ `<<<TASK_RESULT:{taskId}>>>`,
54817
+ `...your result for this task...`,
54818
+ `<<<END_TASK_RESULT>>>`,
54819
+ ``,
54820
+ `If delimiters don't fit your workflow, use \`### Task N of M\` headings and we'll parse those instead.`
54821
+ ].filter(Boolean).join("\n");
54822
+ }
54823
+ function buildScaffoldOutput(groupTasks) {
54824
+ return groupTasks.map(
54825
+ (task, idx) => `<<<TASK_RESULT:${task.id}>>>
54826
+ Scaffold result for task ${idx + 1} of ${groupTasks.length}: ${task.id}
54827
+ <<<END_TASK_RESULT>>>`
54828
+ ).join("\n\n");
54829
+ }
54830
+ var init_run_coalesced_task_group = __esm({
54831
+ "src/runtime/run-coalesced-task-group.ts"() {
54832
+ "use strict";
54833
+ init_artifact_store();
54834
+ init_event_log();
54835
+ init_state_store();
54836
+ init_child_pi();
54837
+ init_role_permission();
54838
+ init_output_splitter();
54839
+ init_team_runner_artifacts();
54840
+ init_worker_heartbeat();
54841
+ init_workspace_tree();
54842
+ }
54843
+ });
54844
+
54736
54845
  // src/runtime/runtime-policy.ts
54737
54846
  function resolveTaskRuntimeKind(globalKind, role, isolationPolicy, env = process.env) {
54738
54847
  if (globalKind === "scaffold") return "scaffold";
@@ -55719,7 +55828,7 @@ function hashToBase36(content, length) {
55719
55828
  }
55720
55829
  function calculateAdaptiveLength(existingCount, config = DEFAULT_CONFIG) {
55721
55830
  for (let length = config.minLength; length <= config.maxLength; length++) {
55722
- const totalPossibilities = Math.pow(36, length);
55831
+ const totalPossibilities = 36 ** length;
55723
55832
  const probability = 1 - Math.exp(-(existingCount * existingCount) / (2 * totalPossibilities));
55724
55833
  if (probability <= config.maxCollisionProbability) {
55725
55834
  return length;
@@ -56812,7 +56921,8 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56812
56921
  } : updated;
56813
56922
  try {
56814
56923
  return withRunLockSync(manifest, () => {
56815
- retryLoop: for (let attempt = 0; attempt < 100; attempt++) {
56924
+ for (let attempt = 0; attempt < 100; attempt++) {
56925
+ flushPendingAtomicWrites();
56816
56926
  const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
56817
56927
  merged = updateTask(latest, taskWithCheckpoint);
56818
56928
  let currentMtime;
@@ -56823,7 +56933,7 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56823
56933
  }
56824
56934
  if (currentMtime !== baseMtime) {
56825
56935
  baseMtime = currentMtime;
56826
- continue retryLoop;
56936
+ continue;
56827
56937
  }
56828
56938
  let recheckMtime;
56829
56939
  try {
@@ -56833,7 +56943,7 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56833
56943
  }
56834
56944
  if (recheckMtime !== baseMtime) {
56835
56945
  baseMtime = recheckMtime;
56836
- continue retryLoop;
56946
+ continue;
56837
56947
  }
56838
56948
  let preWriteMtime;
56839
56949
  try {
@@ -56843,16 +56953,16 @@ function persistSingleTaskUpdate(manifest, fallbackTasks, updated, checkpointPha
56843
56953
  }
56844
56954
  if (preWriteMtime !== baseMtime) {
56845
56955
  baseMtime = preWriteMtime;
56846
- continue retryLoop;
56956
+ continue;
56847
56957
  }
56848
- break retryLoop;
56958
+ break;
56849
56959
  }
56850
56960
  if (merged === void 0) {
56851
56961
  logInternalError("persistSingleTaskUpdate", new Error("failed to converge after 50 attempts"));
56852
56962
  throw new Error("persistSingleTaskUpdate: failed to converge after 50 attempts");
56853
56963
  }
56854
56964
  try {
56855
- saveRunTasks(manifest, merged);
56965
+ saveRunTasksCoalesced(manifest, merged);
56856
56966
  } catch (err2) {
56857
56967
  logInternalError("persistSingleTaskUpdate", err2);
56858
56968
  throw err2;
@@ -56884,6 +56994,7 @@ function checkpointTask(manifest, tasks, task, phase, childPid) {
56884
56994
  var init_state_helpers = __esm({
56885
56995
  "src/runtime/task-runner/state-helpers.ts"() {
56886
56996
  "use strict";
56997
+ init_atomic_write();
56887
56998
  init_locks();
56888
56999
  init_state_store();
56889
57000
  init_internal_error();
@@ -60494,10 +60605,8 @@ var init_team_runner = __esm({
60494
60605
  init_usage();
60495
60606
  init_internal_error();
60496
60607
  init_branch_freshness();
60497
- init_team_runner_artifacts();
60498
60608
  init_cancellation();
60499
60609
  init_coalesce_tasks();
60500
- init_run_coalesced_task_group();
60501
60610
  init_concurrency();
60502
60611
  init_crew_agent_records();
60503
60612
  init_crew_hooks();
@@ -60512,6 +60621,7 @@ var init_team_runner = __esm({
60512
60621
  init_recovery_recipes();
60513
60622
  init_retry_executor();
60514
60623
  init_role_permission();
60624
+ init_run_coalesced_task_group();
60515
60625
  init_run_tracker();
60516
60626
  init_runtime_policy();
60517
60627
  init_task_display();
@@ -60519,6 +60629,7 @@ var init_team_runner = __esm({
60519
60629
  init_task_graph_scheduler();
60520
60630
  init_task_output_context();
60521
60631
  init_task_runner();
60632
+ init_team_runner_artifacts();
60522
60633
  init_usage_tracker();
60523
60634
  init_workflow_state();
60524
60635
  init_adaptive_plan();
@@ -68516,7 +68627,6 @@ function tryScanJson(text) {
68516
68627
  try {
68517
68628
  return JSON.parse(candidate);
68518
68629
  } catch {
68519
- continue;
68520
68630
  }
68521
68631
  }
68522
68632
  return void 0;
@@ -68672,7 +68782,7 @@ function makeWorkflowCtx(manifest, opts) {
68672
68782
  const semaphore = new Semaphore(concurrency);
68673
68783
  let finalResult;
68674
68784
  let agentCount = opts.resumedState ? opts.resumedState.agentCount : 0;
68675
- let phaseState = opts.resumedState ? {
68785
+ const phaseState = opts.resumedState ? {
68676
68786
  currentPhase: opts.resumedState.currentPhase,
68677
68787
  phases: [...opts.resumedState.phases]
68678
68788
  } : { currentPhase: void 0, phases: [] };
@@ -69443,6 +69553,48 @@ ${tail}` : ""}`;
69443
69553
  }, 3e3);
69444
69554
  timer.unref();
69445
69555
  }
69556
+ function resolveAnalysisText(params, cwd) {
69557
+ const hasInline = typeof params.analysis === "string" && params.analysis.length > 0;
69558
+ const hasPath = typeof params.analysisPath === "string" && params.analysisPath.length > 0;
69559
+ if (hasInline && hasPath) {
69560
+ return {
69561
+ error: "`analysis` and `analysisPath` are mutually exclusive. Set exactly one.",
69562
+ source: "none"
69563
+ };
69564
+ }
69565
+ if (!hasInline && !hasPath) return { source: "none" };
69566
+ if (hasPath) {
69567
+ let resolved;
69568
+ try {
69569
+ resolved = resolveContainedPath(cwd, params.analysisPath);
69570
+ } catch {
69571
+ return {
69572
+ error: `analysisPath must be within project directory: ${params.analysisPath}`,
69573
+ source: "none"
69574
+ };
69575
+ }
69576
+ if (!fs90.existsSync(resolved)) {
69577
+ return {
69578
+ error: `Analysis file not found: ${resolved}`,
69579
+ source: "none"
69580
+ };
69581
+ }
69582
+ const { size } = fs90.statSync(resolved);
69583
+ if (size > MAX_ANALYSIS_BYTES) {
69584
+ return {
69585
+ error: `Analysis file too large: ${size} bytes (max ${MAX_ANALYSIS_BYTES}). Trim the analysis or pass a summary inline.`,
69586
+ source: "none"
69587
+ };
69588
+ }
69589
+ const raw = fs90.readFileSync(resolved, "utf-8");
69590
+ const sanitized2 = sanitizeTaskText(raw);
69591
+ if (!sanitized2) return { source: "none" };
69592
+ return { text: sanitized2, source: "path" };
69593
+ }
69594
+ const sanitized = sanitizeTaskText(params.analysis);
69595
+ if (!sanitized) return { source: "none" };
69596
+ return { text: sanitized, source: "inline" };
69597
+ }
69446
69598
  async function handleRun(params, ctx) {
69447
69599
  if (params.chain) {
69448
69600
  const { handleChainRun: handleChainRun2 } = await Promise.resolve().then(() => (init_chain_dispatch(), chain_dispatch_exports));
@@ -69526,11 +69678,14 @@ Commit or stash changes before using worktree mode, or use workspaceMode: 'singl
69526
69678
  id: "01_agent",
69527
69679
  role: params.role ?? "agent",
69528
69680
  task: "{goal}",
69529
- model: params.model
69681
+ model: params.model,
69682
+ reads: params.analysis || params.analysisPath ? ["analysis.md"] : void 0
69530
69683
  }
69531
69684
  ]
69532
69685
  } : workflows.find((item) => item.name === workflowName);
69533
69686
  if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
69687
+ const analysisParam = resolveAnalysisText(params, resolvedCtx.cwd);
69688
+ if (analysisParam.error) return result(analysisParam.error, { action: "run", status: "error" }, true);
69534
69689
  const { expandParallelResearchWorkflow: expandParallelResearch } = await Promise.resolve().then(() => (init_parallel_research(), parallel_research_exports));
69535
69690
  const workflow = directAgent ? baseWorkflow : expandParallelResearch(baseWorkflow, resolvedCtx.cwd);
69536
69691
  if (!directAgent) {
@@ -69548,6 +69703,11 @@ Commit or stash changes before using worktree mode, or use workspaceMode: 'singl
69548
69703
  if (!directAgent && workflow.source === "builtin" && isGoalWrapEnabled(resolvedCtx.cwd, workflow.name)) {
69549
69704
  const decision2 = shouldGoalWrap(resolvedCtx.cwd, workflow);
69550
69705
  if (decision2.enabled) {
69706
+ if (analysisParam.text) {
69707
+ console.warn(
69708
+ `[team-tool.run] analysis param is ignored by goal-wrapped run (workflow=${workflow.name}). The analysis artifact will not be written.`
69709
+ );
69710
+ }
69551
69711
  return await startGoalWrappedRun(params, ctx, workflow, goal);
69552
69712
  }
69553
69713
  if (decision2.message) {
@@ -69623,10 +69783,19 @@ Commit or stash changes before using worktree mode, or use workspaceMode: 'singl
69623
69783
  `,
69624
69784
  producer: "team-tool"
69625
69785
  });
69786
+ const analysisArtifacts = analysisParam.text ? [
69787
+ writeArtifact(paths.artifactsRoot, {
69788
+ kind: "prompt",
69789
+ relativePath: "shared/analysis.md",
69790
+ content: `${analysisParam.text}
69791
+ `,
69792
+ producer: "team-tool"
69793
+ })
69794
+ ] : [];
69626
69795
  const updatedManifest = {
69627
69796
  ...manifest,
69628
69797
  ...skillOverride !== void 0 ? { skillOverride } : {},
69629
- artifacts: [goalArtifact],
69798
+ artifacts: [goalArtifact, ...analysisArtifacts],
69630
69799
  summary: "Run manifest created; worker execution is not implemented yet."
69631
69800
  };
69632
69801
  atomicWriteJson(paths.manifestPath, updatedManifest);
@@ -70140,13 +70309,14 @@ ${dwfResult.manifest.summary ?? ""}`,
70140
70309
  executed.manifest.status === "failed"
70141
70310
  );
70142
70311
  }
70143
- var _cachedExecuteTeamRun, crewInitPromise;
70312
+ var _cachedExecuteTeamRun, crewInitPromise, MAX_ANALYSIS_BYTES;
70144
70313
  var init_run = __esm({
70145
70314
  "src/extension/team-tool/run.ts"() {
70146
70315
  "use strict";
70147
70316
  init_discover_agents();
70148
70317
  init_config();
70149
70318
  init_pipeline_runner();
70319
+ init_task_packet();
70150
70320
  init_active_run_registry();
70151
70321
  init_artifact_store();
70152
70322
  init_atomic_write();
@@ -70166,6 +70336,7 @@ var init_run = __esm({
70166
70336
  init_config_patch();
70167
70337
  init_context();
70168
70338
  init_goal_wrap();
70339
+ MAX_ANALYSIS_BYTES = 1e5;
70169
70340
  }
70170
70341
  });
70171
70342