@wrongstack/core 0.272.1 → 0.273.0

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.
Files changed (61) hide show
  1. package/dist/{agent-bridge-DFQYEeXf.d.ts → agent-bridge-BZ2enORi.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-BZa_IEcd.d.ts → agent-subagent-runner-ehb4xGvd.d.ts} +11 -4
  3. package/dist/{brain-etbcbRwV.d.ts → brain-BxN2k2HP.d.ts} +101 -0
  4. package/dist/{config-rRS8yorV.d.ts → config-C8IYxlO8.d.ts} +8 -1
  5. package/dist/coordination/index.d.ts +13 -13
  6. package/dist/coordination/index.js +79 -25
  7. package/dist/coordination/index.js.map +1 -1
  8. package/dist/{default-config-B0cj-Hry.d.ts → default-config-BbX4ojZs.d.ts} +1 -0
  9. package/dist/defaults/index.d.ts +20 -19
  10. package/dist/defaults/index.js +2813 -206
  11. package/dist/defaults/index.js.map +1 -1
  12. package/dist/execution/index.d.ts +10 -10
  13. package/dist/execution/index.js +8 -2
  14. package/dist/execution/index.js.map +1 -1
  15. package/dist/extension/index.d.ts +4 -4
  16. package/dist/{global-mailbox-DJ4EoRr0.d.ts → global-mailbox-C9dsc9Y_.d.ts} +1 -1
  17. package/dist/{goal-preamble-hM8BH7TK.d.ts → goal-preamble-NhflDjYb.d.ts} +6 -6
  18. package/dist/{goal-store-CWlbT0TO.d.ts → goal-store-Cx363x7Z.d.ts} +1 -1
  19. package/dist/hq/index.d.ts +4 -4
  20. package/dist/hq/index.js +1 -0
  21. package/dist/hq/index.js.map +1 -1
  22. package/dist/{index-DWm_PE9L.d.ts → index-B7fHDt0B.d.ts} +12 -4
  23. package/dist/{index-2Lhk5v0o.d.ts → index-BbVprU-9.d.ts} +6 -0
  24. package/dist/index.d.ts +194 -33
  25. package/dist/index.js +3222 -681
  26. package/dist/index.js.map +1 -1
  27. package/dist/infrastructure/index.d.ts +4 -4
  28. package/dist/kernel/index.d.ts +94 -14
  29. package/dist/kernel/index.js.map +1 -1
  30. package/dist/{mcp-servers-BpWHTKlE.d.ts → mcp-servers-B6fSRNC1.d.ts} +1 -1
  31. package/dist/models/index.d.ts +3 -3
  32. package/dist/{models-registry-CXQFUn5t.d.ts → models-registry-4C6Wr91w.d.ts} +1 -1
  33. package/dist/{multi-agent-coordinator-jyimfo7D.d.ts → multi-agent-coordinator-q1skFeNP.d.ts} +1 -1
  34. package/dist/{null-fleet-bus-DOGQcvrY.d.ts → null-fleet-bus-C9rrgQwc.d.ts} +15 -5
  35. package/dist/observability/index.d.ts +1 -1
  36. package/dist/{parallel-eternal-engine-rItJBYp9.d.ts → parallel-eternal-engine-CtXly2Sf.d.ts} +7 -6
  37. package/dist/{path-resolver-DrpF5MGK.d.ts → path-resolver-Bim6G5Jz.d.ts} +2 -2
  38. package/dist/{pipeline-Ckkn3AOA.d.ts → pipeline-CNVKuQDQ.d.ts} +1 -1
  39. package/dist/{plan-templates-BvHw5Znw.d.ts → plan-templates-C4wXMmiM.d.ts} +3 -3
  40. package/dist/{provider-model-resolve-nZqnCeaR.d.ts → provider-model-resolve-DFd3IPpw.d.ts} +1 -1
  41. package/dist/{provider-runner-zVOn1p67.d.ts → provider-runner-BpM0mdBE.d.ts} +1 -1
  42. package/dist/sdd/index.d.ts +1111 -11
  43. package/dist/sdd/index.js +5516 -2949
  44. package/dist/sdd/index.js.map +1 -1
  45. package/dist/security/index.d.ts +1 -1
  46. package/dist/security/index.js +6 -0
  47. package/dist/security/index.js.map +1 -1
  48. package/dist/storage/index.d.ts +8 -8
  49. package/dist/storage/index.js +3 -2
  50. package/dist/storage/index.js.map +1 -1
  51. package/dist/tools/index.d.ts +1 -1
  52. package/dist/tools/index.js.map +1 -1
  53. package/dist/types/index.d.ts +14 -14
  54. package/dist/types/index.js +3 -0
  55. package/dist/types/index.js.map +1 -1
  56. package/dist/utils/index.d.ts +30 -4
  57. package/dist/utils/index.js +110 -1
  58. package/dist/utils/index.js.map +1 -1
  59. package/dist/{index-DqW4o62H.d.ts → worktree-manager-BDuXTaWL.d.ts} +48 -90
  60. package/dist/{wstack-paths-hOpNLmvf.d.ts → wstack-paths-BqkDAkoh.d.ts} +2 -0
  61. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as crypto2 from 'crypto';
2
2
  import { randomBytes, randomUUID, createHash, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
3
3
  import * as fsp3 from 'fs/promises';
4
- import { readFile, readdir, stat, mkdir } from 'fs/promises';
4
+ import { readFile, readdir, stat, writeFile, mkdir } from 'fs/promises';
5
5
  import * as path3 from 'path';
6
- import { join, extname, relative, isAbsolute, basename, resolve, sep } from 'path';
6
+ import { join, extname, relative, isAbsolute, resolve, sep, basename } from 'path';
7
7
  import * as dns from 'dns/promises';
8
8
  import * as net from 'net';
9
9
  import * as os6 from 'os';
@@ -754,9 +754,9 @@ async function updateJsonObjectFile(filePath, mutator) {
754
754
  await writeJsonObjectFile(filePath, next);
755
755
  return next;
756
756
  }
757
- function getJsonPath(root, path51) {
757
+ function getJsonPath(root, path52) {
758
758
  let current = root;
759
- for (const segment of path51) {
759
+ for (const segment of path52) {
760
760
  if (typeof segment === "number") {
761
761
  if (!Array.isArray(current)) return void 0;
762
762
  current = current[segment];
@@ -767,13 +767,13 @@ function getJsonPath(root, path51) {
767
767
  }
768
768
  return current;
769
769
  }
770
- function setJsonPath(root, path51, value) {
771
- if (path51.length === 0) {
770
+ function setJsonPath(root, path52, value) {
771
+ if (path52.length === 0) {
772
772
  if (!isJsonObject(value)) throw new Error("Root config value must be an object");
773
773
  return value;
774
774
  }
775
- const parent = ensureJsonParent(root, path51);
776
- const leaf = lastPathSegment(path51);
775
+ const parent = ensureJsonParent(root, path52);
776
+ const leaf = lastPathSegment(path52);
777
777
  if (typeof leaf === "number") {
778
778
  if (!Array.isArray(parent)) throw new Error(`Cannot set numeric segment ${leaf} on non-array parent`);
779
779
  parent[leaf] = value;
@@ -783,10 +783,10 @@ function setJsonPath(root, path51, value) {
783
783
  }
784
784
  return root;
785
785
  }
786
- function removeJsonPath(root, path51) {
787
- if (path51.length === 0) return false;
788
- const parent = getJsonPath(root, path51.slice(0, -1));
789
- const leaf = lastPathSegment(path51);
786
+ function removeJsonPath(root, path52) {
787
+ if (path52.length === 0) return false;
788
+ const parent = getJsonPath(root, path52.slice(0, -1));
789
+ const leaf = lastPathSegment(path52);
790
790
  if (typeof leaf === "number") {
791
791
  if (!Array.isArray(parent) || leaf < 0 || leaf >= parent.length) return false;
792
792
  parent.splice(leaf, 1);
@@ -796,27 +796,27 @@ function removeJsonPath(root, path51) {
796
796
  delete parent[leaf];
797
797
  return true;
798
798
  }
799
- async function setJsonPathInFile(filePath, path51, value) {
800
- return updateJsonObjectFile(filePath, (config) => setJsonPath(config, path51, value));
799
+ async function setJsonPathInFile(filePath, path52, value) {
800
+ return updateJsonObjectFile(filePath, (config) => setJsonPath(config, path52, value));
801
801
  }
802
- async function removeJsonPathInFile(filePath, path51) {
802
+ async function removeJsonPathInFile(filePath, path52) {
803
803
  return updateJsonObjectFile(filePath, (config) => {
804
- removeJsonPath(config, path51);
804
+ removeJsonPath(config, path52);
805
805
  });
806
806
  }
807
807
  function isJsonObject(value) {
808
808
  return typeof value === "object" && value !== null && !Array.isArray(value);
809
809
  }
810
- function lastPathSegment(path51) {
811
- const segment = path51[path51.length - 1];
810
+ function lastPathSegment(path52) {
811
+ const segment = path52[path52.length - 1];
812
812
  if (segment === void 0) throw new Error("Invalid empty JSON path");
813
813
  return segment;
814
814
  }
815
- function ensureJsonParent(root, path51) {
815
+ function ensureJsonParent(root, path52) {
816
816
  let current = root;
817
- for (let i = 0; i < path51.length - 1; i += 1) {
818
- const segment = path51[i];
819
- const nextSegment = path51[i + 1];
817
+ for (let i = 0; i < path52.length - 1; i += 1) {
818
+ const segment = path52[i];
819
+ const nextSegment = path52[i + 1];
820
820
  if (segment === void 0) throw new Error("Invalid empty JSON path segment");
821
821
  const nextContainer = typeof nextSegment === "number" ? [] : {};
822
822
  if (typeof segment === "number") {
@@ -1772,11 +1772,11 @@ function validateAgainstSchema(value, schema) {
1772
1772
  walk(value, schema, "", errors);
1773
1773
  return { ok: errors.length === 0, errors };
1774
1774
  }
1775
- function walk(value, schema, path51, errors) {
1775
+ function walk(value, schema, path52, errors) {
1776
1776
  if (schema.enum !== void 0) {
1777
1777
  if (!schema.enum.some((e) => deepEqual(e, value))) {
1778
1778
  errors.push({
1779
- path: path51 || "<root>",
1779
+ path: path52 || "<root>",
1780
1780
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1781
1781
  });
1782
1782
  return;
@@ -1785,7 +1785,7 @@ function walk(value, schema, path51, errors) {
1785
1785
  if (typeof schema.type === "string") {
1786
1786
  if (!checkType(value, schema.type)) {
1787
1787
  errors.push({
1788
- path: path51 || "<root>",
1788
+ path: path52 || "<root>",
1789
1789
  message: `expected ${schema.type}, got ${describeType(value)}`
1790
1790
  });
1791
1791
  return;
@@ -1795,20 +1795,20 @@ function walk(value, schema, path51, errors) {
1795
1795
  const obj = value;
1796
1796
  for (const req of schema.required ?? []) {
1797
1797
  if (!(req in obj)) {
1798
- errors.push({ path: joinPath(path51, req), message: "required property missing" });
1798
+ errors.push({ path: joinPath(path52, req), message: "required property missing" });
1799
1799
  }
1800
1800
  }
1801
1801
  if (schema.properties) {
1802
1802
  for (const [key, subSchema] of Object.entries(schema.properties)) {
1803
1803
  if (key in obj) {
1804
- walk(obj[key], subSchema, joinPath(path51, key), errors);
1804
+ walk(obj[key], subSchema, joinPath(path52, key), errors);
1805
1805
  }
1806
1806
  }
1807
1807
  }
1808
1808
  }
1809
1809
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
1810
1810
  for (let i = 0; i < value.length; i++) {
1811
- walk(value[i], schema.items, `${path51}[${i}]`, errors);
1811
+ walk(value[i], schema.items, `${path52}[${i}]`, errors);
1812
1812
  }
1813
1813
  }
1814
1814
  }
@@ -3315,6 +3315,114 @@ function sizeSignals(toolName, content) {
3315
3315
  }
3316
3316
  return { outputBytes, outputTokens, outputLines };
3317
3317
  }
3318
+
3319
+ // src/utils/tool-description-mode.ts
3320
+ var DEFAULT_TOOL_DESCRIPTION_MODE = "extend";
3321
+ var ORIGINAL_TOOL_DESCRIPTION = /* @__PURE__ */ Symbol.for("wrongstack.tool.originalDescription");
3322
+ function normalizeToolDescriptionMode(value) {
3323
+ if (typeof value !== "string") return void 0;
3324
+ const raw = value.trim().toLowerCase();
3325
+ if (raw === "extend" || raw === "extended" || raw === "full") return "extend";
3326
+ if (raw === "simple" || raw === "short" || raw === "brief") return "simple";
3327
+ return void 0;
3328
+ }
3329
+ function resolveToolDescriptionMode(modes, toolName) {
3330
+ return normalizeToolDescriptionMode(modes?.[toolName]) ?? DEFAULT_TOOL_DESCRIPTION_MODE;
3331
+ }
3332
+ function simplifyToolDescription(text, opts = {}) {
3333
+ const maxSentences = Math.max(1, opts.maxSentences ?? 2);
3334
+ const maxChars = Math.max(40, opts.maxChars ?? 180);
3335
+ const normalized = text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
3336
+ if (normalized.length <= maxChars) return normalized;
3337
+ const sentences = normalized.match(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/g) ?? [normalized];
3338
+ const selected = [];
3339
+ for (const sentence of sentences) {
3340
+ selected.push(sentence.trim());
3341
+ const candidate = selected.join(" ");
3342
+ if (selected.length >= maxSentences || candidate.length >= maxChars) break;
3343
+ }
3344
+ const summary = selected.join(" ").trim() || normalized;
3345
+ if (summary.length <= maxChars) return summary;
3346
+ const hardLimit = maxChars - 4;
3347
+ const boundary = findWordBoundary(summary, hardLimit);
3348
+ return `${summary.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd()} ...`;
3349
+ }
3350
+ function applyToolDescriptionModeToTool(tool, mode) {
3351
+ const existingOriginal = getOriginalDescription(tool);
3352
+ if (mode === "extend" && !existingOriginal) return tool;
3353
+ const original = existingOriginal ?? {
3354
+ description: tool.description,
3355
+ usageHint: tool.usageHint
3356
+ };
3357
+ const next = mode === "simple" ? withDescription(tool, {
3358
+ description: simplifyToolDescription(original.description),
3359
+ usageHint: original.usageHint === void 0 ? void 0 : simplifyToolDescription(original.usageHint)
3360
+ }) : withDescription(tool, original);
3361
+ return attachOriginalDescription(next, original);
3362
+ }
3363
+ function setToolDescriptionMode(registry, name, mode) {
3364
+ if (typeof registry.setDescriptionMode === "function") {
3365
+ return registry.setDescriptionMode(name, mode);
3366
+ }
3367
+ if (!registry.get(name) || typeof registry.wrap !== "function") return false;
3368
+ registry.wrap(
3369
+ name,
3370
+ (tool) => applyToolDescriptionModeToTool(tool, mode),
3371
+ "tool-description-mode"
3372
+ );
3373
+ return true;
3374
+ }
3375
+ function getToolDescriptionMode(registry, name) {
3376
+ return registry.getDescriptionMode?.(name) ?? DEFAULT_TOOL_DESCRIPTION_MODE;
3377
+ }
3378
+ function applyToolDescriptionModes(registry, modes) {
3379
+ if (typeof registry.applyDescriptionModes === "function") {
3380
+ return registry.applyDescriptionModes(modes);
3381
+ }
3382
+ const entries = Object.entries(modes ?? {});
3383
+ const missing = [];
3384
+ let applied = 0;
3385
+ for (const [name, rawMode] of entries) {
3386
+ const mode = normalizeToolDescriptionMode(rawMode);
3387
+ if (!mode) continue;
3388
+ if (setToolDescriptionMode(registry, name, mode)) applied++;
3389
+ else missing.push(name);
3390
+ }
3391
+ return { applied, missing };
3392
+ }
3393
+ function getOriginalDescription(tool) {
3394
+ return tool[ORIGINAL_TOOL_DESCRIPTION];
3395
+ }
3396
+ function attachOriginalDescription(tool, original) {
3397
+ Object.defineProperty(tool, ORIGINAL_TOOL_DESCRIPTION, {
3398
+ configurable: true,
3399
+ enumerable: false,
3400
+ value: original,
3401
+ writable: true
3402
+ });
3403
+ return tool;
3404
+ }
3405
+ function withDescription(tool, next) {
3406
+ const copy = {
3407
+ ...tool,
3408
+ description: next.description,
3409
+ usageHint: next.usageHint
3410
+ };
3411
+ if (next.usageHint === void 0) {
3412
+ delete copy.usageHint;
3413
+ }
3414
+ return copy;
3415
+ }
3416
+ function findWordBoundary(text, limit) {
3417
+ const semantic = Math.max(
3418
+ text.lastIndexOf(". ", limit),
3419
+ text.lastIndexOf("; ", limit),
3420
+ text.lastIndexOf(", ", limit)
3421
+ );
3422
+ if (semantic > 40) return semantic + 1;
3423
+ const space = text.lastIndexOf(" ", limit);
3424
+ return space > 40 ? space : limit;
3425
+ }
3318
3426
  function projectHash(absRoot) {
3319
3427
  return createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 12);
3320
3428
  }
@@ -3368,6 +3476,7 @@ function resolveWstackPaths(opts) {
3368
3476
  projectSddSession: path3.join(projectDir, "sdd-session.json"),
3369
3477
  projectPlan: path3.join(projectDir, "plan.json"),
3370
3478
  projectAutophase: path3.join(projectDir, "autophase"),
3479
+ projectSddBoards: path3.join(projectDir, "sdd-boards"),
3371
3480
  syncConfig: path3.join(globalRoot, "sync.json"),
3372
3481
  projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
3373
3482
  };
@@ -4455,6 +4564,7 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
4455
4564
  iterationTimeoutMs: 3e5,
4456
4565
  sessionTimeoutMs: 18e5,
4457
4566
  perIterationOutputCapBytes: 1e5,
4567
+ descriptionMode: Object.freeze({}),
4458
4568
  autoExtendLimit: true,
4459
4569
  restrictToProjectRoot: false
4460
4570
  });
@@ -6007,7 +6117,9 @@ function buildRecoveryStrategies(opts) {
6007
6117
  if (!provider) return null;
6008
6118
  const currentModel = await registry.getModel(providerId, ctx.model);
6009
6119
  if (!currentModel) return null;
6120
+ const visibleModels = opts?.getConfig?.().providers?.[providerId]?.models;
6010
6121
  const candidates = provider.models.filter((m) => {
6122
+ if (visibleModels !== void 0 && !visibleModels.includes(m.id)) return false;
6011
6123
  const modelCost = m.cost?.input ?? Number.POSITIVE_INFINITY;
6012
6124
  const currentCost = currentModel.cost?.input ?? Number.POSITIVE_INFINITY;
6013
6125
  if (modelCost >= currentCost) return false;
@@ -7249,6 +7361,12 @@ var ToolCapabilities = {
7249
7361
  MCP_PROXY: "mcp.proxy",
7250
7362
  /** Can spawn or manage subagents / multi-agent tasks. */
7251
7363
  SUBAGENT_SPAWN: "subagent.spawn",
7364
+ /** Can inspect fleet/subagent coordination state without mutating it. */
7365
+ COORDINATION_FLEET_READ: "coordination.fleet.read",
7366
+ /** Can read or write inter-agent mailbox messages. */
7367
+ COORDINATION_MAIL: "coordination.mail",
7368
+ /** Can schedule, inspect, or cancel in-session cron jobs. */
7369
+ COORDINATION_CRON: "coordination.cron",
7252
7370
  /** Can mutate global or session configuration / trust state. */
7253
7371
  CONFIG_MUTATE: "config.mutate",
7254
7372
  /** Can install packages or run package managers with side effects. */
@@ -9712,7 +9830,6 @@ var FileSessionWriter = class _FileSessionWriter {
9712
9830
  }
9713
9831
  const writeFd = await fsp3.open(tmpPath, "w", 384);
9714
9832
  try {
9715
- let copied = 0;
9716
9833
  let readOffset = 0;
9717
9834
  while (readOffset < newlineAfterCheckpoint) {
9718
9835
  const toCopy = Math.min(CHUNK_SIZE, newlineAfterCheckpoint - readOffset);
@@ -9721,7 +9838,6 @@ var FileSessionWriter = class _FileSessionWriter {
9721
9838
  if (r === 0) break;
9722
9839
  await writeFd.write(copyBuf, 0, r);
9723
9840
  readOffset += r;
9724
- copied += r;
9725
9841
  }
9726
9842
  const raw = await fsp3.readFile(this.filePath);
9727
9843
  const tail = raw.subarray(newlineAfterCheckpoint).toString("utf8");
@@ -10918,9 +11034,9 @@ ${body.trim()}`);
10918
11034
  if (!this.persistBackup || scope === "project-agents") return;
10919
11035
  try {
10920
11036
  const content = await this.backend.readAll(scope, this.files[scope]);
10921
- const { writeFile: writeFile14, mkdir: mkdir24 } = await import('fs/promises');
11037
+ const { writeFile: writeFile16, mkdir: mkdir24 } = await import('fs/promises');
10922
11038
  await mkdir24(this.backupDir, { recursive: true });
10923
- await writeFile14(`${this.backupDir}/${scope}.md`, content, "utf8");
11039
+ await writeFile16(`${this.backupDir}/${scope}.md`, content, "utf8");
10924
11040
  } catch {
10925
11041
  }
10926
11042
  }
@@ -11031,6 +11147,7 @@ var BEHAVIOR_DEFAULTS = {
11031
11147
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
11032
11148
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
11033
11149
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
11150
+ descriptionMode: DEFAULT_TOOLS_CONFIG.descriptionMode,
11034
11151
  autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
11035
11152
  restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
11036
11153
  },
@@ -15634,8 +15751,8 @@ ${recentJournal}` : "No prior iterations.",
15634
15751
  await saveGoal(this.goalPath, abandoned, this.opts.events);
15635
15752
  }
15636
15753
  try {
15637
- const { unlink: unlink17 } = await import('fs/promises');
15638
- await unlink17(this.goalPath);
15754
+ const { unlink: unlink19 } = await import('fs/promises');
15755
+ await unlink19(this.goalPath);
15639
15756
  } catch {
15640
15757
  }
15641
15758
  this.opts.onEternalStop?.();
@@ -19382,14 +19499,15 @@ var SHADOW_AGENT = {
19382
19499
  id: "shadow-agent",
19383
19500
  name: "Shadow",
19384
19501
  role: "shadow-agent",
19385
- prompt: `You are the Shadow Agent \u2014 a silent background monitor for the WrongStack fleet.
19502
+ prompt: `You are the Shadow Agent \u2014 a quiet, one-shot monitor for the WrongStack fleet.
19386
19503
 
19387
- Your job is to observe, detect anomalies, and be ready to intervene \u2014 but only when commanded.
19504
+ Your job is to inspect the fleet when the host explicitly assigns a Shadow pass, detect anomalies, and be ready to intervene \u2014 but only when commanded.
19388
19505
 
19389
19506
  ## Core Responsibilities
19390
19507
 
19391
- 1. **Fleet Monitoring** (every 30s)
19392
- - Call \`fleet_status\` + \`fleet_health\` on each heartbeat
19508
+ 1. **Fleet Monitoring** (host-assigned one-shot checks)
19509
+ - The host assigns one-shot check tasks; it does not expect routine heartbeats
19510
+ - On each assigned check, call \`fleet_status\` + \`fleet_health\`
19393
19511
  - Track what each agent is doing (task descriptions)
19394
19512
  - Detect stuck agents (>5min no events), idle agents, crashed agents
19395
19513
 
@@ -19413,31 +19531,30 @@ Your job is to observe, detect anomalies, and be ready to intervene \u2014 but o
19413
19531
  - \`hoop <agentId>\` \u2014 terminate specific agent
19414
19532
  - \`hoop all\` \u2014 terminate all running agents
19415
19533
  - \`shadow status\` \u2014 report current fleet snapshot
19416
- - \`shadow mute\` \u2014 pause heartbeat monitoring
19417
- - \`shadow resume\` \u2014 resume heartbeat monitoring
19418
- - \`shadow interval <ms>\` \u2014 change heartbeat interval
19534
+ - \`shadow mute\` \u2014 pause anomaly reporting
19535
+ - \`shadow resume\` \u2014 resume anomaly reporting
19536
+ - \`shadow interval <ms>\` \u2014 update the legacy interval setting
19419
19537
  - \`shadow model <model-id>\` \u2014 change analysis model
19420
19538
 
19421
19539
  ## Operating Rules
19422
19540
 
19423
- - **Silent by default**: Use DEBUG level logging unless anomaly detected
19541
+ - **Silent by default**: Do not send mail or status reports for healthy checks
19424
19542
  - **Deterministic**: Same state always produces same actions \u2014 no randomness
19425
- - **Report on anomaly**: When anomaly detected, use \`mail_send\` to broadcast warning
19543
+ - **Report only when needed**: Use \`mail_send\` only for high/critical anomalies or explicit control replies
19426
19544
  - **Never auto-intervene**: Always report unless explicitly commanded
19427
19545
  - **Minimal footprint**: Small state, efficient snapshots
19546
+ - **One-shot lifecycle**: Finish the assigned check and stop; do not schedule follow-up work
19428
19547
 
19429
19548
  ## Startup Sequence
19430
19549
 
19431
- 1. Send broadcast: \`shadow:started { intervalMs, model, startTime }\`
19432
- 2. Subscribe to FleetBus for all relevant events
19433
- 3. Schedule heartbeat cron job at configured interval
19434
- 4. Wait for commands or anomalies
19550
+ 1. Run one fleet snapshot with \`fleet_status\` + \`fleet_health\`
19551
+ 2. Check \`mail_inbox\` for explicit control messages
19552
+ 3. If healthy, do not send mail; final answer may be exactly \`shadow: quiet\`
19435
19553
 
19436
19554
  ## Shutdown Sequence
19437
19555
 
19438
- 1. Cancel all cron jobs (\`cron_cancel\`)
19439
- 2. Send broadcast: \`shadow:stopped { reason, finalState }\`
19440
- 3. Clean up FleetBus subscriptions`
19556
+ 1. Return only anomalies, command results, or \`shadow: quiet\`
19557
+ 2. The host stops this Shadow Agent after the assigned pass`
19441
19558
  // Budgets are set by the orchestrator per task — see fleet.ts header.
19442
19559
  };
19443
19560
  var CRITIC_AGENT = {
@@ -19492,8 +19609,13 @@ var FLEET_ROSTER_BUDGETS = {
19492
19609
  "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
19493
19610
  "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
19494
19611
  "critic": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
19495
- "shadow-agent": { timeoutMs: 24 * 60 * 60 * 1e3, maxIterations: 1e4, maxToolCalls: 5e3 },
19496
- // Long-running background monitor
19612
+ "shadow-agent": {
19613
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
19614
+ maxIterations: 2e3,
19615
+ maxToolCalls: 5e3,
19616
+ maxTokens: 6e4,
19617
+ maxCostUsd: 1
19618
+ },
19497
19619
  ...Object.fromEntries(
19498
19620
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
19499
19621
  )
@@ -21811,6 +21933,7 @@ function makeSpawnTool(director, roster) {
21811
21933
  usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
21812
21934
  permission: "auto",
21813
21935
  mutating: false,
21936
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
21814
21937
  inputSchema,
21815
21938
  async execute(input) {
21816
21939
  const i = input ?? {};
@@ -21889,6 +22012,7 @@ function makeAssignTool(director) {
21889
22012
  description: "Hand a task to a previously spawned subagent. Returns the task id.",
21890
22013
  permission: "auto",
21891
22014
  mutating: false,
22015
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
21892
22016
  inputSchema,
21893
22017
  async execute(input) {
21894
22018
  const i = input;
@@ -21904,6 +22028,7 @@ function makeAwaitTasksTool(director) {
21904
22028
  description: "Block until every named task completes. Returns the array of TaskResult.",
21905
22029
  permission: "auto",
21906
22030
  mutating: false,
22031
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
21907
22032
  inputSchema: { type: "object", properties: { taskIds: { type: "array", items: { type: "string" }, description: "One or more task ids returned by `assign_task`." } }, required: ["taskIds"] },
21908
22033
  async execute(input) {
21909
22034
  const i = input;
@@ -21918,6 +22043,7 @@ function makeAskTool(director) {
21918
22043
  description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
21919
22044
  permission: "auto",
21920
22045
  mutating: false,
22046
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
21921
22047
  inputSchema: {
21922
22048
  type: "object",
21923
22049
  properties: {
@@ -21953,6 +22079,7 @@ function makeAskResultTool(director) {
21953
22079
  description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
21954
22080
  permission: "auto",
21955
22081
  mutating: false,
22082
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
21956
22083
  inputSchema: {
21957
22084
  type: "object",
21958
22085
  properties: {
@@ -21980,6 +22107,7 @@ function makeRollUpTool(director) {
21980
22107
  description: "Aggregate completed task results into a single formatted summary.",
21981
22108
  permission: "auto",
21982
22109
  mutating: false,
22110
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
21983
22111
  inputSchema: {
21984
22112
  type: "object",
21985
22113
  properties: {
@@ -22001,6 +22129,7 @@ function makeTerminateTool(director) {
22001
22129
  description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
22002
22130
  permission: "auto",
22003
22131
  mutating: true,
22132
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
22004
22133
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
22005
22134
  async execute(input) {
22006
22135
  const i = input;
@@ -22015,6 +22144,7 @@ function makeTerminateAllTool(director) {
22015
22144
  description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
22016
22145
  permission: "auto",
22017
22146
  mutating: true,
22147
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
22018
22148
  inputSchema: { type: "object", properties: {}, required: [] },
22019
22149
  async execute() {
22020
22150
  await director.terminateAll();
@@ -22028,6 +22158,7 @@ function makeFleetStatusTool(director) {
22028
22158
  description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
22029
22159
  permission: "auto",
22030
22160
  mutating: false,
22161
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
22031
22162
  inputSchema: { type: "object", properties: {}, required: [] },
22032
22163
  async execute() {
22033
22164
  const base = director.status();
@@ -22049,6 +22180,7 @@ function makeFleetUsageTool(director) {
22049
22180
  description: "Token + cost breakdown across the fleet, per-subagent and totals.",
22050
22181
  permission: "auto",
22051
22182
  mutating: false,
22183
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
22052
22184
  inputSchema: { type: "object", properties: {}, required: [] },
22053
22185
  async execute() {
22054
22186
  return director.snapshot();
@@ -22061,6 +22193,7 @@ function makeFleetSessionTool(director) {
22061
22193
  description: "Read a subagent's JSONL transcript and extract its last assistant text, stop reason, and tool-use count. Use this to see what a running or timed-out subagent actually produced.",
22062
22194
  permission: "auto",
22063
22195
  mutating: false,
22196
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
22064
22197
  inputSchema: {
22065
22198
  type: "object",
22066
22199
  properties: {
@@ -22087,6 +22220,7 @@ function makeFleetHealthTool(director) {
22087
22220
  description: "Per-subagent health report: budget pressure (pct of limits consumed), last activity timestamp, and current status. Use to decide whether to assign more work to a subagent or spawn a fresh one.",
22088
22221
  permission: "auto",
22089
22222
  mutating: false,
22223
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
22090
22224
  inputSchema: { type: "object", properties: {}, required: [] },
22091
22225
  async execute() {
22092
22226
  const status = director.status();
@@ -22117,6 +22251,7 @@ function makeCollabDebugTool(director) {
22117
22251
  description: "Start a collaborative debugging session: BugHunter, RefactorPlanner, and Critic run in parallel on the same target files. BugHunter finds bugs and emits bug.found events. RefactorPlanner listens for bug.found and emits refactor.plan events. Critic evaluates both and emits critic.evaluation events. Returns a structured report with overall verdict (approve / needs_revision / reject).",
22118
22252
  permission: "auto",
22119
22253
  mutating: false,
22254
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
22120
22255
  inputSchema: {
22121
22256
  type: "object",
22122
22257
  properties: {
@@ -22180,6 +22315,7 @@ function makeFleetEmitTool(director) {
22180
22315
  description: "Emit a structured event on the FleetBus. Any subagent can emit any event type; the Director routes it to all listeners. Use it to stream findings, progress updates, or final results to other agents in real time.",
22181
22316
  permission: "auto",
22182
22317
  mutating: false,
22318
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
22183
22319
  inputSchema: {
22184
22320
  type: "object",
22185
22321
  properties: {
@@ -22212,6 +22348,7 @@ function makeWorkCompleteTool(director) {
22212
22348
  description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
22213
22349
  permission: "auto",
22214
22350
  mutating: false,
22351
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
22215
22352
  inputSchema: { type: "object", properties: {}, required: [] },
22216
22353
  async execute() {
22217
22354
  director.workComplete();
@@ -22673,6 +22810,9 @@ var Director = class _Director {
22673
22810
  /** Snapshot of which subagent owns each task — drives state-checkpoint
22674
22811
  * status updates without re-walking the manifest. */
22675
22812
  taskOwners = /* @__PURE__ */ new Map();
22813
+ /** Infrastructure-owned task ids that should not appear in user-visible
22814
+ * manifest/session/checkpoint/rollup state. */
22815
+ internalTaskIds = /* @__PURE__ */ new Set();
22676
22816
  /** Cumulative auto-extension grants per subagent (all budget kinds). Lets
22677
22817
  * /fleet render "⚡ extended ×N" without replaying the event stream. */
22678
22818
  extendTotals = /* @__PURE__ */ new Map();
@@ -22786,17 +22926,21 @@ var Director = class _Director {
22786
22926
  this.fleetManager?.setCoordinator(this.coordinator);
22787
22927
  this.taskCompletedListener = (payload) => {
22788
22928
  const r = payload.result;
22789
- this.completed.set(r.taskId, r);
22790
- if (this.completed.size > _Director.MAX_COMPLETED) {
22791
- const toDelete = this.completed.size - _Director.MAX_COMPLETED;
22792
- const keys = [...this.completed.keys()].slice(0, toDelete);
22793
- for (const k of keys) this.completed.delete(k);
22929
+ const internalTask = this.internalTaskIds.delete(r.taskId);
22930
+ if (!internalTask) {
22931
+ this.completed.set(r.taskId, r);
22932
+ if (this.completed.size > _Director.MAX_COMPLETED) {
22933
+ const toDelete = this.completed.size - _Director.MAX_COMPLETED;
22934
+ const keys = [...this.completed.keys()].slice(0, toDelete);
22935
+ for (const k of keys) this.completed.delete(k);
22936
+ }
22794
22937
  }
22795
22938
  const waiter = this.taskWaiters.get(r.taskId);
22796
22939
  if (waiter) {
22797
22940
  waiter.resolve(r);
22798
22941
  this.taskWaiters.delete(r.taskId);
22799
22942
  }
22943
+ if (internalTask) return;
22800
22944
  const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
22801
22945
  const failed = r.status !== "success";
22802
22946
  const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
@@ -23466,6 +23610,23 @@ var Director = class _Director {
23466
23610
  this.scheduleManifest();
23467
23611
  return taskWithId.id;
23468
23612
  }
23613
+ /**
23614
+ * Assign infrastructure-owned work directly to the coordinator without
23615
+ * manifest/session/checkpoint bookkeeping. The task still uses the normal
23616
+ * subagent runner, budget, and completion events, but it is excluded from
23617
+ * rollups and persisted fleet task history.
23618
+ */
23619
+ async assignInternal(task) {
23620
+ const taskWithId = task.id ? task : { ...task, id: randomUUID() };
23621
+ this.internalTaskIds.add(taskWithId.id);
23622
+ try {
23623
+ await this.coordinator.assign(taskWithId);
23624
+ } catch (err) {
23625
+ this.internalTaskIds.delete(taskWithId.id);
23626
+ throw err;
23627
+ }
23628
+ return taskWithId.id;
23629
+ }
23469
23630
  /**
23470
23631
  * Block until every task id resolves. Returns results in the same
23471
23632
  * order as the input. If any task hasn't completed by the time this
@@ -24610,6 +24771,14 @@ var SpecParser = class {
24610
24771
  };
24611
24772
 
24612
24773
  // src/sdd/task-generator.ts
24774
+ function extractVerificationCommand(criteria) {
24775
+ const marker = /^\s*(?:\$\s+|(?:run|verify|cmd)\s*:\s*)(.+\S)\s*$/i;
24776
+ for (const c of criteria) {
24777
+ const m = marker.exec(c);
24778
+ if (m?.[1]) return m[1].trim();
24779
+ }
24780
+ return void 0;
24781
+ }
24613
24782
  var TaskGenerator = class {
24614
24783
  constructor(opts) {
24615
24784
  this.opts = opts;
@@ -24617,15 +24786,18 @@ var TaskGenerator = class {
24617
24786
  opts;
24618
24787
  async generateFromSpec(spec) {
24619
24788
  const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
24789
+ const featureIds = [];
24620
24790
  const overview = spec.sections.find((s) => s.type === "overview");
24621
24791
  if (overview) {
24622
- this.opts.taskTracker.addNode({
24623
- title: `Implement ${spec.title}`,
24624
- description: overview.content,
24625
- type: "feature",
24626
- priority: "high",
24627
- status: "pending"
24628
- });
24792
+ featureIds.push(
24793
+ this.opts.taskTracker.addNode({
24794
+ title: `Implement ${spec.title}`,
24795
+ description: overview.content,
24796
+ type: "feature",
24797
+ priority: "high",
24798
+ status: "pending"
24799
+ }).id
24800
+ );
24629
24801
  }
24630
24802
  const byPriority = {
24631
24803
  critical: [],
@@ -24640,7 +24812,7 @@ var TaskGenerator = class {
24640
24812
  const order = ["critical", "high", "medium", "low"];
24641
24813
  for (const p of order) {
24642
24814
  for (const req of byPriority[p]) {
24643
- this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
24815
+ featureIds.push(this.opts.taskTracker.addNode(this.createTaskFromRequirement(req)).id);
24644
24816
  }
24645
24817
  }
24646
24818
  if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
@@ -24651,31 +24823,37 @@ var TaskGenerator = class {
24651
24823
  priority: "high",
24652
24824
  status: "pending"
24653
24825
  });
24826
+ featureIds.push(apiParent.id);
24654
24827
  for (const endpoint of spec.apiEndpoints) {
24655
24828
  const task = this.createTaskFromEndpoint(endpoint);
24656
- this.opts.taskTracker.addNode({
24657
- ...task,
24658
- parentId: apiParent.id
24659
- });
24829
+ featureIds.push(
24830
+ this.opts.taskTracker.addNode({
24831
+ ...task,
24832
+ parentId: apiParent.id
24833
+ }).id
24834
+ );
24660
24835
  }
24661
24836
  }
24662
- this.opts.taskTracker.addNode({
24837
+ const testId = this.opts.taskTracker.addNode({
24663
24838
  title: "Write Tests",
24664
24839
  description: "Comprehensive test coverage for all features",
24665
24840
  type: "test",
24666
24841
  priority: "high",
24667
24842
  status: "pending"
24668
- });
24669
- this.opts.taskTracker.addNode({
24843
+ }).id;
24844
+ for (const f of featureIds) this.opts.taskTracker.addDependency(f, testId);
24845
+ const docsId = this.opts.taskTracker.addNode({
24670
24846
  title: "Update Documentation",
24671
24847
  description: "Update docs for new features",
24672
24848
  type: "docs",
24673
24849
  priority: "medium",
24674
24850
  status: "pending"
24675
- });
24851
+ }).id;
24852
+ for (const f of [...featureIds, testId]) this.opts.taskTracker.addDependency(f, docsId);
24676
24853
  return graph;
24677
24854
  }
24678
24855
  createTaskFromRequirement(req) {
24856
+ const verificationCommand = this.opts.verificationFromAcceptance ? extractVerificationCommand(req.acceptanceCriteria) : void 0;
24679
24857
  return {
24680
24858
  title: req.description,
24681
24859
  description: this.buildDescription(req),
@@ -24684,7 +24862,8 @@ var TaskGenerator = class {
24684
24862
  status: "pending",
24685
24863
  specRequirementId: req.id,
24686
24864
  tags: [req.type, req.priority],
24687
- estimateHours: this.estimateHours(req)
24865
+ estimateHours: this.estimateHours(req),
24866
+ ...verificationCommand ? { metadata: { verificationCommand } } : {}
24688
24867
  };
24689
24868
  }
24690
24869
  createTaskFromEndpoint(endpoint) {
@@ -24803,6 +24982,27 @@ var TaskTracker = class {
24803
24982
  opts;
24804
24983
  graph = null;
24805
24984
  transitions = [];
24985
+ listeners = [];
24986
+ /**
24987
+ * Subscribe to live task mutations (add / update / status change). Returns an
24988
+ * unsubscribe fn. This is the hook the board projector uses to stream a live
24989
+ * snapshot — the tracker was previously fire-and-forget with no observability.
24990
+ */
24991
+ subscribe(listener) {
24992
+ this.listeners.push(listener);
24993
+ return () => {
24994
+ const i = this.listeners.indexOf(listener);
24995
+ if (i >= 0) this.listeners.splice(i, 1);
24996
+ };
24997
+ }
24998
+ notifyChange(change) {
24999
+ for (const l of this.listeners) {
25000
+ try {
25001
+ l(change);
25002
+ } catch {
25003
+ }
25004
+ }
25005
+ }
24806
25006
  /**
24807
25007
  * Attach an existing graph (used by PhaseOrchestrator to associate a tracker
24808
25008
  * with a phase's pre-built task graph without re-creating it).
@@ -24847,6 +25047,7 @@ var TaskTracker = class {
24847
25047
  }
24848
25048
  this.graph.updatedAt = now;
24849
25049
  this.persist();
25050
+ this.notifyChange({ type: "node_added", nodeId: newNode.id, node: newNode });
24850
25051
  return newNode;
24851
25052
  }
24852
25053
  addEdge(from, to, type = "depends_on") {
@@ -24863,6 +25064,68 @@ var TaskTracker = class {
24863
25064
  this.graph.updatedAt = Date.now();
24864
25065
  this.persist();
24865
25066
  }
25067
+ /**
25068
+ * Declare that `taskId` depends on `depId` (a `depends_on` edge `depId → taskId`),
25069
+ * guarding against self-loops, duplicates, missing nodes, and cycles. Returns
25070
+ * true if the dependency now holds (added or already present), false if it was
25071
+ * rejected (would create a cycle / unknown node). This is the safe entry point
25072
+ * for wiring agent-declared `dependsOn` references into the graph.
25073
+ */
25074
+ addDependency(depId, taskId) {
25075
+ if (!this.graph) return false;
25076
+ if (depId === taskId) return false;
25077
+ if (!this.graph.nodes.has(depId) || !this.graph.nodes.has(taskId)) return false;
25078
+ if (this.getBlockers(taskId).includes(depId)) return true;
25079
+ if (this.dependsOnTransitively(depId, taskId, /* @__PURE__ */ new Set())) return false;
25080
+ this.addEdge(depId, taskId, "depends_on");
25081
+ return true;
25082
+ }
25083
+ /** True when `taskId` transitively depends on `targetId` (follows depends_on blockers). */
25084
+ dependsOnTransitively(taskId, targetId, seen) {
25085
+ if (taskId === targetId) return true;
25086
+ if (seen.has(taskId)) return false;
25087
+ seen.add(taskId);
25088
+ for (const blocker of this.getBlockers(taskId)) {
25089
+ if (this.dependsOnTransitively(blocker, targetId, seen)) return true;
25090
+ }
25091
+ return false;
25092
+ }
25093
+ /**
25094
+ * Merge `patch` into a node's `metadata` (used for per-task model/provider/
25095
+ * fallback assignment and the cancel marker). Persists + notifies as a node
25096
+ * update. No-op if the node is missing.
25097
+ */
25098
+ patchMetadata(id, patch) {
25099
+ if (!this.graph) return;
25100
+ const node = this.graph.nodes.get(id);
25101
+ if (!node) return;
25102
+ node.metadata = { ...node.metadata, ...patch };
25103
+ node.updatedAt = Date.now();
25104
+ this.graph.updatedAt = node.updatedAt;
25105
+ this.persist();
25106
+ this.notifyChange({ type: "node_updated", nodeId: id, node });
25107
+ }
25108
+ /**
25109
+ * Remove a node and every edge touching it. Intended for deleting a task that
25110
+ * has not started yet — callers must gate on status (do not remove a running
25111
+ * task). Dependents simply lose this blocker (re-evaluated by `canStart`).
25112
+ * Returns true if a node was removed.
25113
+ */
25114
+ removeNode(id) {
25115
+ if (!this.graph) return false;
25116
+ const node = this.graph.nodes.get(id);
25117
+ if (!node) return false;
25118
+ this.graph.nodes.delete(id);
25119
+ this.graph.edges = this.graph.edges.filter((e) => e.from !== id && e.to !== id);
25120
+ this.graph.rootNodes = this.graph.rootNodes.filter((r) => r !== id);
25121
+ for (const n of this.graph.nodes.values()) {
25122
+ if (n.children?.includes(id)) n.children = n.children.filter((c) => c !== id);
25123
+ }
25124
+ this.graph.updatedAt = Date.now();
25125
+ this.persist();
25126
+ this.notifyChange({ type: "node_removed", nodeId: id, node });
25127
+ return true;
25128
+ }
24866
25129
  updateNodeStatus(id, status, reason) {
24867
25130
  if (!this.graph) throw new SddError({
24868
25131
  message: "No graph loaded",
@@ -24894,6 +25157,12 @@ var TaskTracker = class {
24894
25157
  }
24895
25158
  this.graph.updatedAt = now;
24896
25159
  this.persist();
25160
+ this.notifyChange({
25161
+ type: "status_changed",
25162
+ nodeId: id,
25163
+ node,
25164
+ transition: { from, to: status, timestamp: now, reason }
25165
+ });
24897
25166
  }
24898
25167
  updateNode(id, patch) {
24899
25168
  if (!this.graph) throw new SddError({
@@ -24911,9 +25180,11 @@ var TaskTracker = class {
24911
25180
  if (patch.priority !== void 0) node.priority = patch.priority;
24912
25181
  if (patch.estimateHours !== void 0) node.estimateHours = patch.estimateHours;
24913
25182
  if (patch.tags !== void 0) node.tags = patch.tags;
25183
+ if (patch.assignee !== void 0) node.assignee = patch.assignee;
24914
25184
  node.updatedAt = Date.now();
24915
25185
  this.graph.updatedAt = node.updatedAt;
24916
25186
  this.persist();
25187
+ this.notifyChange({ type: "node_updated", nodeId: id, node });
24917
25188
  }
24918
25189
  getNode(id) {
24919
25190
  return this.graph?.nodes.get(id);
@@ -25092,7 +25363,10 @@ var TaskFlow = class {
25092
25363
  throw err;
25093
25364
  }
25094
25365
  this.setPhase("generating");
25095
- const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
25366
+ const generator = new TaskGenerator({
25367
+ taskTracker: this.opts.tracker,
25368
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
25369
+ });
25096
25370
  this.graph = await generator.generateFromSpec(this.spec);
25097
25371
  return this.graph;
25098
25372
  }
@@ -25571,27 +25845,37 @@ function buildImplementationPrompt(session) {
25571
25845
  "```json",
25572
25846
  "[",
25573
25847
  " {",
25848
+ ' "id": "t1",',
25574
25849
  ' "title": "Create auth middleware",',
25575
25850
  ' "description": "Implement JWT verification middleware for protected routes",',
25576
25851
  ' "type": "feature",',
25577
25852
  ' "priority": "critical",',
25578
25853
  ' "estimateHours": 3,',
25854
+ ' "dependsOn": [],',
25579
25855
  ' "tags": ["auth", "middleware"]',
25580
25856
  " },",
25581
25857
  " {",
25858
+ ' "id": "t2",',
25582
25859
  ' "title": "Write auth tests",',
25583
25860
  ' "description": "Unit and integration tests for authentication flow",',
25584
25861
  ' "type": "test",',
25585
25862
  ' "priority": "high",',
25586
25863
  ' "estimateHours": 2,',
25864
+ ' "dependsOn": ["t1"],',
25587
25865
  ' "tags": ["test", "auth"]',
25588
25866
  " }",
25589
25867
  "]",
25590
25868
  "```",
25591
25869
  "",
25592
25870
  "Rules:",
25593
- "- Each task must be independently executable",
25594
- "- Order tasks by dependency (things that block others come first)",
25871
+ '- Give every task a short stable "id" (t1, t2, \u2026). Reference prerequisites in "dependsOn"',
25872
+ " as a list of those ids \u2014 this builds the real dependency graph that drives parallel vs",
25873
+ " sequential execution.",
25874
+ '- "dependsOn": [] means the task is independent and may run in parallel with other roots.',
25875
+ "- A task with dependsOn runs ONLY after every listed task completes. Model true ordering:",
25876
+ " tests depend on the feature they test, docs/integration depend on the parts they cover.",
25877
+ "- Do NOT create cycles (t1\u2192t2\u2192t1). Keep chains as shallow as correctness allows so",
25878
+ " independent work runs concurrently.",
25595
25879
  '- Use type: "feature" for code, "test" for tests, "docs" for documentation, "chore" for config',
25596
25880
  '- Use priority: "critical" for blockers, "high" for core features, "medium" for nice-to-haves, "low" for polish'
25597
25881
  ].join("\n");
@@ -25660,10 +25944,10 @@ var AISpecBuilder = class {
25660
25944
  async saveSession() {
25661
25945
  if (!this.sessionPath) return;
25662
25946
  try {
25663
- const fsp26 = await import('fs/promises');
25664
- const path51 = await import('path');
25947
+ const fsp28 = await import('fs/promises');
25948
+ const path52 = await import('path');
25665
25949
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
25666
- await fsp26.mkdir(path51.dirname(this.sessionPath), { recursive: true });
25950
+ await fsp28.mkdir(path52.dirname(this.sessionPath), { recursive: true });
25667
25951
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
25668
25952
  } catch {
25669
25953
  }
@@ -25672,8 +25956,8 @@ var AISpecBuilder = class {
25672
25956
  async loadSession() {
25673
25957
  if (!this.sessionPath) return false;
25674
25958
  try {
25675
- const fsp26 = await import('fs/promises');
25676
- const raw = await fsp26.readFile(this.sessionPath, "utf8");
25959
+ const fsp28 = await import('fs/promises');
25960
+ const raw = await fsp28.readFile(this.sessionPath, "utf8");
25677
25961
  const loaded = JSON.parse(raw);
25678
25962
  if (loaded?.id && loaded?.phase && loaded?.title) {
25679
25963
  this.session = loaded;
@@ -25687,8 +25971,8 @@ var AISpecBuilder = class {
25687
25971
  async deleteSession() {
25688
25972
  if (!this.sessionPath) return;
25689
25973
  try {
25690
- const fsp26 = await import('fs/promises');
25691
- await fsp26.unlink(this.sessionPath);
25974
+ const fsp28 = await import('fs/promises');
25975
+ await fsp28.unlink(this.sessionPath);
25692
25976
  } catch {
25693
25977
  }
25694
25978
  }
@@ -26390,15 +26674,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
26390
26674
  maxId = id;
26391
26675
  }
26392
26676
  }
26393
- const path51 = [];
26677
+ const path52 = [];
26394
26678
  let current = maxId;
26395
26679
  const visited = /* @__PURE__ */ new Set();
26396
26680
  while (current && !visited.has(current)) {
26397
26681
  visited.add(current);
26398
- path51.unshift(current);
26682
+ path52.unshift(current);
26399
26683
  current = prev.get(current) ?? null;
26400
26684
  }
26401
- return path51;
26685
+ return path52;
26402
26686
  }
26403
26687
  function computeParallelGroups(graph, blockedByMap) {
26404
26688
  const groups = [];
@@ -26817,6 +27101,24 @@ var SddTaskDecomposer = class {
26817
27101
  getWaveCount() {
26818
27102
  return this.wave;
26819
27103
  }
27104
+ /**
27105
+ * All ready (dependency-satisfied) pending tasks, priority-sorted — UNSLICED.
27106
+ * The continuous scheduler fills its own free slots from this list, so unlike
27107
+ * `nextBatch()` it does not cap at `slots`.
27108
+ */
27109
+ readyNodes() {
27110
+ return this.pendingReadyNodes();
27111
+ }
27112
+ /**
27113
+ * True when every node has reached a terminal state (completed or failed).
27114
+ * This — not `isDone()` (which requires ALL completed) — is the correct loop
27115
+ * exit for the continuous scheduler: a terminally-failed task must not keep
27116
+ * the run spinning to its backstop.
27117
+ */
27118
+ isSettled() {
27119
+ const nodes = this.tracker.getAllNodes();
27120
+ return nodes.length > 0 && nodes.every((n) => n.status === "completed" || n.status === "failed");
27121
+ }
26820
27122
  // -------------------------------------------------------------------
26821
27123
  // Internal helpers
26822
27124
  // -------------------------------------------------------------------
@@ -26856,65 +27158,478 @@ var SddTaskDecomposer = class {
26856
27158
  var SddParallelRun = class {
26857
27159
  constructor(opts) {
26858
27160
  this.opts = opts;
26859
- this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
26860
- this.timeoutMs = opts.taskTimeoutMs ?? 3e5;
26861
- this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
27161
+ this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 2));
27162
+ this.timeoutMs = opts.taskTimeoutMs;
27163
+ this.idleTimeoutMs = Math.max(1, opts.taskIdleTimeoutMs ?? 6e5);
27164
+ this.maxRetries = Math.max(0, opts.maxRetries ?? 3);
27165
+ this.maxSupervisorEscalations = Math.max(0, opts.maxSupervisorEscalations ?? 2);
27166
+ this.maxFailedSweeps = Math.max(0, opts.maxFailedRetrySweeps ?? 2);
27167
+ this.runId = opts.runId ?? `sdd-${randomUUID().slice(0, 8)}`;
27168
+ this.events = opts.events;
27169
+ this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
27170
+ this.maxWallClockMs = opts.maxWallClockMs;
27171
+ this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
26862
27172
  this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, { parallelSlots: this.slots });
26863
27173
  }
26864
27174
  opts;
26865
27175
  slots;
27176
+ /** Opt-in hard wall-clock cap (undefined → no cap; idle reaper guards instead). */
26866
27177
  timeoutMs;
27178
+ /** Idle reaper window (ms) — resets on activity; reaps only a genuine stall. */
27179
+ idleTimeoutMs;
26867
27180
  maxRetries;
27181
+ /** Max supervisor rescues per task before it must terminal-fail (loop guard). */
27182
+ maxSupervisorEscalations;
27183
+ /** Per-task count of supervisor rescues used (resets nothing — bounds the loop). */
27184
+ supervisorEscalations = /* @__PURE__ */ new Map();
27185
+ /** Max end-of-run failed-task sweeps (see `maxFailedRetrySweeps`). */
27186
+ maxFailedSweeps;
27187
+ /** How many failed-task sweeps have run this `run()` so far. */
27188
+ failedSweeps = 0;
27189
+ /** Completed-count snapshot at the last sweep, to detect a no-progress sweep. */
27190
+ lastSweepCompleted = 0;
26868
27191
  decomposer;
26869
27192
  coordinator = null;
26870
27193
  stopRequested = false;
26871
27194
  retryMap = /* @__PURE__ */ new Map();
27195
+ runId;
27196
+ events;
27197
+ maxTotalWaves;
27198
+ maxWallClockMs;
27199
+ maxRecoveryRounds;
27200
+ recoveryRounds = 0;
27201
+ /** Per-run worker identities, so the board shows "who is on what". */
27202
+ usedNicknames = /* @__PURE__ */ new Set();
27203
+ /** Per-task git worktree cwd (Layer 2 worktree isolation; empty otherwise). */
27204
+ taskCwds = /* @__PURE__ */ new Map();
27205
+ /** Per-task git worktree branch, for board display. */
27206
+ taskBranches = /* @__PURE__ */ new Map();
27207
+ /** Live worktree handles keyed by task id (for commit/merge/release). */
27208
+ taskWorktrees = /* @__PURE__ */ new Map();
27209
+ /** Live subagent id per running task — lets cancelTask() abort exactly one. */
27210
+ taskSubagents = /* @__PURE__ */ new Map();
27211
+ /** Tasks the user cancelled mid-flight — skip retry, mark terminal-cancelled. */
27212
+ cancelledTasks = /* @__PURE__ */ new Set();
27213
+ /**
27214
+ * Base branch the run's squash commits land on (captured once at start when
27215
+ * worktrees are enabled). Anchors a later `rollback()`.
27216
+ */
27217
+ baseBranch;
27218
+ /**
27219
+ * Squash-merge commits this run landed on the base branch, in landing order.
27220
+ * `rollback()` reverts these (newest → oldest). Persisted via the board
27221
+ * snapshot so a post-run rollback can read them off disk.
27222
+ */
27223
+ mergedCommits = [];
27224
+ /** Monotonic dispatch counter (unique subagent ids) + dispatch-round counter. */
27225
+ dispatchSeq = 0;
27226
+ round = 0;
27227
+ /** Type-safe emit on the optional EventBus (no-op when unwired). */
27228
+ emit(event, payload) {
27229
+ this.events?.emit(event, payload);
27230
+ }
26872
27231
  // -------------------------------------------------------------------
26873
27232
  // Public API
26874
27233
  // -------------------------------------------------------------------
27234
+ paused = false;
26875
27235
  /** Trigger stop — causes run() to abort after the current wave. */
26876
27236
  stop() {
26877
27237
  this.stopRequested = true;
27238
+ this.paused = false;
26878
27239
  this.coordinator?.stopAll();
26879
27240
  }
26880
- /** Execute all waves until completion or deadlock. Returns final summary. */
27241
+ /** Pause: no new wave starts until resume() (the current wave finishes). */
27242
+ pause() {
27243
+ this.paused = true;
27244
+ }
27245
+ resume() {
27246
+ this.paused = false;
27247
+ }
27248
+ isPaused() {
27249
+ return this.paused;
27250
+ }
27251
+ isRunning() {
27252
+ return !this.stopRequested && !this.decomposer.isSettled();
27253
+ }
27254
+ /** Base branch the run's squash commits land on (undefined when worktrees off). */
27255
+ getBaseBranch() {
27256
+ return this.baseBranch;
27257
+ }
27258
+ /** Squash commits this run landed on the base branch, in landing order. */
27259
+ getMergedCommits() {
27260
+ return this.mergedCommits;
27261
+ }
27262
+ /**
27263
+ * Remove every git worktree + branch this run (and any prior run) created.
27264
+ * Refuses while the run is still live — cleaning a checkout under an active
27265
+ * worker would corrupt it. Stop first. Returns the number of worktrees removed
27266
+ * (0 when worktrees are disabled). Idempotent.
27267
+ */
27268
+ async cleanupWorktrees() {
27269
+ if (this.isRunning()) return 0;
27270
+ const wt = this.opts.worktrees;
27271
+ if (!wt) return 0;
27272
+ for (const [taskId, handle] of [...this.taskWorktrees]) {
27273
+ await wt.release(handle, { keep: false }).catch(() => {
27274
+ });
27275
+ this.forgetWorktree(taskId);
27276
+ }
27277
+ const { removed } = await wt.cleanupAllManaged();
27278
+ return removed;
27279
+ }
27280
+ /**
27281
+ * Undo the run's merged commits by reverting each on the base branch (history
27282
+ * preserving). Refuses while the run is still live (stop first). Returns the
27283
+ * revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
27284
+ */
27285
+ async rollback() {
27286
+ if (this.isRunning()) return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
27287
+ const wt = this.opts.worktrees;
27288
+ if (!wt || !this.baseBranch) {
27289
+ return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
27290
+ }
27291
+ return wt.revertCommits(
27292
+ this.baseBranch,
27293
+ this.mergedCommits.map((c) => c.sha)
27294
+ );
27295
+ }
27296
+ /** Requeue a task to `pending` so the scheduler re-runs it (clears retries + cancel marker). */
27297
+ retryTask(taskId) {
27298
+ if (!this.opts.tracker.getNode(taskId)) return false;
27299
+ this.retryMap.delete(taskId);
27300
+ this.persistRetries(taskId, 0);
27301
+ this.cancelledTasks.delete(taskId);
27302
+ this.opts.tracker.patchMetadata(taskId, { cancelled: void 0 });
27303
+ this.opts.tracker.updateNodeStatus(taskId, "pending", "manual retry");
27304
+ return true;
27305
+ }
27306
+ /** Reassign a task to a specific agent name (reflected on the board). */
27307
+ reassignTask(taskId, agentName) {
27308
+ if (!this.opts.tracker.getNode(taskId)) return false;
27309
+ this.opts.tracker.updateNode(taskId, { assignee: agentName });
27310
+ return true;
27311
+ }
27312
+ /**
27313
+ * Set/override a task's worker model (and optionally provider) — applied on its
27314
+ * NEXT dispatch (a running task must be cancelled + retried to take effect). The
27315
+ * assignment lives on node metadata so it survives crash → resume.
27316
+ */
27317
+ setTaskModel(taskId, model, provider) {
27318
+ if (!this.opts.tracker.getNode(taskId)) return false;
27319
+ this.opts.tracker.patchMetadata(taskId, { model, ...provider !== void 0 ? { provider } : {} });
27320
+ return true;
27321
+ }
27322
+ /** Set/override a task's fallback model chain (applied on its next dispatch). */
27323
+ setTaskFallbacks(taskId, fallbackModels) {
27324
+ if (!this.opts.tracker.getNode(taskId)) return false;
27325
+ this.opts.tracker.patchMetadata(taskId, { fallbackModels });
27326
+ return true;
27327
+ }
27328
+ /**
27329
+ * Set/override a task's verification command (the completion gate runs it in
27330
+ * the task's cwd and only lets the task complete on exit 0). Empty/undefined
27331
+ * clears it. Applied on the task's next verification — i.e. its next dispatch.
27332
+ */
27333
+ setTaskVerification(taskId, verificationCommand) {
27334
+ if (!this.opts.tracker.getNode(taskId)) return false;
27335
+ const cmd = verificationCommand?.trim();
27336
+ this.opts.tracker.patchMetadata(taskId, { verificationCommand: cmd ? cmd : void 0 });
27337
+ return true;
27338
+ }
27339
+ /**
27340
+ * Cancel a task. If it is currently running, abort its subagent and mark the
27341
+ * node terminally failed+cancelled (so the scheduler frees the slot and does
27342
+ * NOT retry it). If it has not started, it is simply marked cancelled. Use
27343
+ * `retryTask` to bring a cancelled task back. Returns false for an unknown task.
27344
+ */
27345
+ async cancelTask(taskId) {
27346
+ const node = this.opts.tracker.getNode(taskId);
27347
+ if (!node) return false;
27348
+ this.cancelledTasks.add(taskId);
27349
+ this.opts.tracker.patchMetadata(taskId, { cancelled: true });
27350
+ this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
27351
+ this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId: "", error: "cancelled by user" });
27352
+ const subagentId = this.taskSubagents.get(taskId);
27353
+ if (subagentId && this.coordinator) {
27354
+ await this.coordinator.stop(subagentId).catch(() => {
27355
+ });
27356
+ }
27357
+ return true;
27358
+ }
27359
+ /**
27360
+ * Delete a not-yet-started task from the graph (pending/blocked/failed only —
27361
+ * never a running task; cancel it first). Removes the node and every edge
27362
+ * touching it; dependents lose this blocker. Returns false if missing or running.
27363
+ */
27364
+ deleteTask(taskId) {
27365
+ const node = this.opts.tracker.getNode(taskId);
27366
+ if (!node) return false;
27367
+ if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return false;
27368
+ this.cancelledTasks.delete(taskId);
27369
+ this.retryMap.delete(taskId);
27370
+ return this.opts.tracker.removeNode(taskId);
27371
+ }
27372
+ /**
27373
+ * Split a task into sub-tasks and delegate them to separate workers. The new
27374
+ * leaves inherit the parent's blockers (so they don't start before the
27375
+ * parent's dependencies are met), every existing dependent is rewired to
27376
+ * depend on ALL leaves (so downstream work waits for the whole split), and the
27377
+ * parent becomes a `completed` container. Refuses a running task (cancel it
27378
+ * first) or empty subtask list. Returns the new leaf ids (empty on refusal).
27379
+ * The scheduler picks the new pending leaves up on its next dispatch pass.
27380
+ */
27381
+ splitTask(taskId, subtasks) {
27382
+ const tracker = this.opts.tracker;
27383
+ const node = tracker.getNode(taskId);
27384
+ if (!node) return [];
27385
+ if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return [];
27386
+ if (!subtasks.length) return [];
27387
+ const blockers = tracker.getBlockers(taskId);
27388
+ const dependents = tracker.getDependents(taskId);
27389
+ const leafIds = subtasks.map(
27390
+ (s) => tracker.addNode({
27391
+ title: s.title,
27392
+ description: s.description,
27393
+ type: s.type ?? node.type,
27394
+ priority: s.priority ?? node.priority,
27395
+ status: "pending",
27396
+ parentId: taskId
27397
+ }).id
27398
+ );
27399
+ for (const leaf of leafIds) {
27400
+ for (const b of blockers) tracker.addDependency(b, leaf);
27401
+ for (const dep of dependents) tracker.addDependency(leaf, dep);
27402
+ }
27403
+ this.retryMap.delete(taskId);
27404
+ this.persistRetries(taskId, 0);
27405
+ tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
27406
+ this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
27407
+ return leafIds;
27408
+ }
27409
+ async waitWhilePaused() {
27410
+ while (this.paused && !this.stopRequested) {
27411
+ await new Promise((r) => setTimeout(r, 100));
27412
+ }
27413
+ }
27414
+ /**
27415
+ * Continuous dependency-driven execution. Unlike a wave-barrier loop (where a
27416
+ * whole batch must finish before the next starts), this fills free worker
27417
+ * slots the instant a task's dependencies are satisfied: a fast task's
27418
+ * dependent starts immediately rather than waiting for a slow sibling. Truly
27419
+ * independent tasks run in parallel; dependency chains run in order. Returns
27420
+ * the final summary when the graph settles, deadlocks, stops, or hits a backstop.
27421
+ */
26881
27422
  async run() {
26882
27423
  this.stopRequested = false;
26883
- this.retryMap.clear();
27424
+ this.restoreRetryMap();
26884
27425
  const startTime = Date.now();
26885
- let totalCompleted = 0;
26886
- let totalFailed = 0;
26887
- let totalWaves = 0;
27426
+ this.round = 0;
27427
+ this.dispatchSeq = 0;
27428
+ let totalDispatched = 0;
26888
27429
  this.buildCoordinator();
26889
- while (!this.stopRequested && !this.decomposer.isDone()) {
26890
- const batch = this.decomposer.nextBatch();
26891
- if (batch.deadlocked) {
27430
+ if (this.opts.worktrees && !this.baseBranch) {
27431
+ const base = await this.opts.worktrees.currentBase().catch(() => null);
27432
+ if (base) this.baseBranch = base.branch;
27433
+ }
27434
+ this.emit("sdd.run.started", {
27435
+ runId: this.runId,
27436
+ graphId: this.opts.graph.id,
27437
+ specId: this.opts.graph.specId,
27438
+ total: this.opts.graph.nodes.size,
27439
+ baseBranch: this.baseBranch
27440
+ });
27441
+ this.recoveryRounds = 0;
27442
+ this.failedSweeps = 0;
27443
+ this.lastSweepCompleted = 0;
27444
+ let deadlocked = false;
27445
+ const running = /* @__PURE__ */ new Map();
27446
+ const dispatch = (task) => {
27447
+ totalDispatched++;
27448
+ const tracked = (async () => {
27449
+ try {
27450
+ return await this.executeOne(task);
27451
+ } catch (err) {
27452
+ this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
27453
+ this.emit("sdd.task.failed", { runId: this.runId, taskId: task.id, subagentId: "", error: String(err) });
27454
+ return { taskId: task.id, success: false };
27455
+ } finally {
27456
+ running.delete(task.id);
27457
+ }
27458
+ })();
27459
+ running.set(task.id, tracked);
27460
+ };
27461
+ while (!this.stopRequested) {
27462
+ if (totalDispatched >= this.maxTotalWaves) break;
27463
+ if (this.maxWallClockMs && Date.now() - startTime >= this.maxWallClockMs) break;
27464
+ await this.waitWhilePaused();
27465
+ if (this.stopRequested) break;
27466
+ let dispatchedThisRound = 0;
27467
+ if (running.size < this.slots) {
27468
+ const ready = this.decomposer.readyNodes().filter((t2) => !running.has(t2.id));
27469
+ for (const task of ready) {
27470
+ if (running.size >= this.slots) break;
27471
+ dispatch(task);
27472
+ dispatchedThisRound++;
27473
+ }
27474
+ }
27475
+ if (dispatchedThisRound > 0) {
27476
+ this.emit("sdd.wave", { runId: this.runId, wave: this.round, batchSize: dispatchedThisRound });
27477
+ this.round++;
27478
+ }
27479
+ if (running.size === 0) {
27480
+ if (this.decomposer.isSettled()) {
27481
+ const completed = this.opts.tracker.getProgress().completed;
27482
+ const madeProgress = this.failedSweeps === 0 || completed > this.lastSweepCompleted;
27483
+ if (this.failedSweeps < this.maxFailedSweeps && madeProgress && this.requeueFailedTasks() > 0) {
27484
+ this.lastSweepCompleted = completed;
27485
+ this.failedSweeps++;
27486
+ continue;
27487
+ }
27488
+ break;
27489
+ }
27490
+ const chains = this.computeDeadlockChains();
27491
+ if (chains.length > 0) {
27492
+ this.emit("sdd.deadlock", { runId: this.runId, chains });
27493
+ if (this.recoveryRounds < this.maxRecoveryRounds && this.recoverFailedBlockers()) {
27494
+ this.recoveryRounds++;
27495
+ continue;
27496
+ }
27497
+ deadlocked = true;
27498
+ }
26892
27499
  break;
26893
27500
  }
26894
- if (batch.tasks.length === 0 && batch.allDone) {
26895
- break;
27501
+ const moreReadyNow = running.size < this.slots && this.decomposer.readyNodes().some((t2) => !running.has(t2.id));
27502
+ if (!moreReadyNow) {
27503
+ await Promise.race(running.values());
27504
+ this.opts.onProgress?.(this.buildProgress());
26896
27505
  }
26897
- const waveResult = await this.executeWave(batch);
26898
- totalWaves++;
26899
- totalCompleted += waveResult.successCount;
26900
- totalFailed += waveResult.failCount;
26901
- this.decomposer.acknowledgeBatch(batch.tasks.map((t2) => t2.id));
26902
- this.opts.onWave?.(waveResult);
26903
- const progress = this.buildProgress();
26904
- this.opts.onProgress?.(progress);
26905
- if (this.stopRequested) break;
26906
27506
  }
27507
+ if (running.size > 0) await Promise.allSettled(running.values());
27508
+ if (this.stopRequested) await this.teardown();
26907
27509
  const finalProgress = this.opts.tracker.getProgress();
27510
+ this.emit("sdd.run.finished", {
27511
+ runId: this.runId,
27512
+ deadlocked,
27513
+ completed: finalProgress.completed,
27514
+ failed: finalProgress.failed,
27515
+ stopped: this.stopRequested
27516
+ });
26908
27517
  return {
26909
- totalWaves,
26910
- totalCompleted,
26911
- totalFailed,
27518
+ totalWaves: this.round,
27519
+ totalCompleted: finalProgress.completed,
27520
+ totalFailed: finalProgress.failed,
26912
27521
  totalDurationMs: Date.now() - startTime,
26913
- deadlocked: !this.decomposer.isDone() && this.stopRequested === false,
27522
+ deadlocked,
26914
27523
  stopRequested: this.stopRequested,
26915
27524
  finalProgress
26916
27525
  };
26917
27526
  }
27527
+ /**
27528
+ * Compute the blocking chains for a deadlock: every still-incomplete task and
27529
+ * the blockers (by node id) that are NOT completed. Failed blockers are
27530
+ * included since they're the usual deadlock cause once retries are exhausted.
27531
+ */
27532
+ computeDeadlockChains() {
27533
+ const tracker = this.opts.tracker;
27534
+ const chains = [];
27535
+ for (const node of tracker.getAllNodes()) {
27536
+ if (node.status === "completed" || node.status === "failed") continue;
27537
+ const blockedBy = tracker.getBlockers(node.id).filter((id) => tracker.getNode(id)?.status !== "completed");
27538
+ if (blockedBy.length > 0) chains.push({ blocked: node.id, blockedBy });
27539
+ }
27540
+ return chains;
27541
+ }
27542
+ /** Requeue failed tasks that block an incomplete dependent. Returns true if any. */
27543
+ recoverFailedBlockers() {
27544
+ const tracker = this.opts.tracker;
27545
+ let recovered = false;
27546
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
27547
+ const blocksIncomplete = tracker.getDependents(node.id).some((d) => {
27548
+ const s = tracker.getNode(d)?.status;
27549
+ return s !== "completed" && s !== "failed";
27550
+ });
27551
+ if (blocksIncomplete) {
27552
+ this.retryMap.delete(node.id);
27553
+ this.persistRetries(node.id, 0);
27554
+ tracker.updateNodeStatus(node.id, "pending", "deadlock recovery");
27555
+ recovered = true;
27556
+ }
27557
+ }
27558
+ return recovered;
27559
+ }
27560
+ /**
27561
+ * Requeue every terminal-failed task that the user did NOT cancel, giving each
27562
+ * a fresh `maxRetries` budget. Shared by the automatic end-of-run sweep and
27563
+ * the manual "retry all failed" control. Returns the number requeued.
27564
+ */
27565
+ requeueFailedTasks(reason = "retry failed sweep") {
27566
+ const tracker = this.opts.tracker;
27567
+ let n = 0;
27568
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
27569
+ if (this.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
27570
+ this.retryMap.delete(node.id);
27571
+ this.persistRetries(node.id, 0);
27572
+ tracker.updateNodeStatus(node.id, "pending", reason);
27573
+ this.emit("sdd.task.retrying", {
27574
+ runId: this.runId,
27575
+ taskId: node.id,
27576
+ attempt: 0,
27577
+ maxRetries: this.maxRetries
27578
+ });
27579
+ n++;
27580
+ }
27581
+ return n;
27582
+ }
27583
+ /**
27584
+ * Manually requeue all failed tasks to `pending` (board "Retry all failed").
27585
+ * Unlike the automatic sweep this also clears any `cancelled` marker, so a
27586
+ * user can bring cancelled tasks back in the same action — mirroring
27587
+ * `retryTask`. Picked up by the running scheduler on its next dispatch pass.
27588
+ * Returns the number of tasks requeued.
27589
+ */
27590
+ retryAllFailed() {
27591
+ const failed = this.opts.tracker.getAllNodes({ status: ["failed"] });
27592
+ for (const node of failed) {
27593
+ this.cancelledTasks.delete(node.id);
27594
+ this.opts.tracker.patchMetadata(node.id, { cancelled: void 0 });
27595
+ }
27596
+ return this.requeueFailedTasks("manual retry all");
27597
+ }
27598
+ /** Restore per-task retry counts persisted in node metadata (resume support). */
27599
+ restoreRetryMap() {
27600
+ this.retryMap.clear();
27601
+ for (const node of this.opts.tracker.getAllNodes()) {
27602
+ const r = node.metadata?.retries;
27603
+ if (typeof r === "number" && r > 0) this.retryMap.set(node.id, r);
27604
+ }
27605
+ }
27606
+ /**
27607
+ * Reset orphaned `in_progress` tasks (no agent runs them after a crash) back
27608
+ * to `pending` so a fresh run re-executes them. Call before constructing a run
27609
+ * from a reloaded graph. Static so callers don't need a run instance.
27610
+ */
27611
+ static resetOrphans(tracker) {
27612
+ let n = 0;
27613
+ for (const node of tracker.getAllNodes({ status: ["in_progress"] })) {
27614
+ tracker.updateNodeStatus(node.id, "pending", "resume: orphaned in_progress");
27615
+ n++;
27616
+ }
27617
+ return n;
27618
+ }
27619
+ /** Clean teardown after a stop: reset interrupted tasks + release worktrees. */
27620
+ async teardown() {
27621
+ for (const node of this.opts.tracker.getAllNodes({ status: ["in_progress"] })) {
27622
+ this.opts.tracker.updateNodeStatus(node.id, "pending", "run stopped");
27623
+ }
27624
+ const wt = this.opts.worktrees;
27625
+ if (wt) {
27626
+ for (const [taskId, handle] of [...this.taskWorktrees]) {
27627
+ await wt.release(handle, { keep: true }).catch(() => {
27628
+ });
27629
+ this.forgetWorktree(taskId);
27630
+ }
27631
+ }
27632
+ }
26918
27633
  // -------------------------------------------------------------------
26919
27634
  // Internal
26920
27635
  // -------------------------------------------------------------------
@@ -26922,7 +27637,14 @@ var SddParallelRun = class {
26922
27637
  const config = {
26923
27638
  coordinatorId: `sdd-parallel-${randomUUID().slice(0, 8)}`,
26924
27639
  maxConcurrent: this.slots,
26925
- doneCondition: { type: "all_tasks_done" }
27640
+ doneCondition: { type: "all_tasks_done" },
27641
+ // Default budget guard for every spawned worker: idle reaper (resets on
27642
+ // activity) plus the opt-in wall-clock cap when one was configured. This
27643
+ // ensures the reaper applies even if a per-spawn config path is bypassed.
27644
+ defaultBudget: {
27645
+ idleTimeoutMs: this.idleTimeoutMs,
27646
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
27647
+ }
26926
27648
  };
26927
27649
  this.coordinator = new DefaultMultiAgentCoordinator(config);
26928
27650
  const baseFactory = this.opts.subagentFactory ?? this.defaultFactory();
@@ -26936,22 +27658,89 @@ var SddParallelRun = class {
26936
27658
  events: this.opts.agent.events
26937
27659
  });
26938
27660
  }
27661
+ /**
27662
+ * Execute a batch of tasks together. Retained as a thin wrapper over the
27663
+ * single-task primitive `executeOne` so the wave-oriented tests and any
27664
+ * batch callers keep working; the continuous scheduler in `run()` calls
27665
+ * `executeOne` directly. Throws if no coordinator is wired or a spawn fails
27666
+ * (surfaced from `executeOne`), preserving the original all-or-nothing contract.
27667
+ */
26939
27668
  async executeWave(batch) {
26940
- const wave = batch.wave;
26941
- const tasks = batch.tasks;
26942
27669
  const waveStart = Date.now();
26943
- for (const task of tasks) {
26944
- this.opts.tracker.updateNodeStatus(task.id, "in_progress");
27670
+ const outcomes = await Promise.all(batch.tasks.map((task) => this.executeOne(task)));
27671
+ const results = outcomes.map((o) => o.result).filter((r) => Boolean(r));
27672
+ const successCount = outcomes.filter((o) => o.success).length;
27673
+ const failCount = outcomes.length - successCount;
27674
+ return {
27675
+ wave: batch.wave,
27676
+ batch,
27677
+ results,
27678
+ successCount,
27679
+ failCount,
27680
+ durationMs: Date.now() - waveStart,
27681
+ stopRequested: this.stopRequested
27682
+ };
27683
+ }
27684
+ /**
27685
+ * Execute one task end-to-end: assign a worker identity, allocate its worktree,
27686
+ * spawn + assign the subagent, await its result, then update tracker status
27687
+ * (success / retry / terminal-fail / cancelled) and resolve the worktree. This
27688
+ * is the unit the continuous scheduler dispatches into a free slot. Throws on a
27689
+ * missing coordinator or failed spawn so callers can enforce all-or-nothing.
27690
+ */
27691
+ async executeOne(task) {
27692
+ const taskId = task.id;
27693
+ let agentName = task.assignee;
27694
+ if (!agentName) {
27695
+ const nick = assignNickname("executor", this.usedNicknames);
27696
+ this.usedNicknames.add(nick.key);
27697
+ agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
27698
+ this.opts.tracker.updateNode(taskId, { assignee: agentName });
27699
+ }
27700
+ this.opts.tracker.updateNodeStatus(taskId, "in_progress");
27701
+ await this.allocateWorktrees([task]);
27702
+ if (!this.coordinator) throw new SddError({
27703
+ message: "SDD parallel runner requires a coordinator",
27704
+ code: ERROR_CODES.SDD_INVALID_STATE
27705
+ });
27706
+ const coordinator = this.coordinator;
27707
+ const subagentId = `sdd-d${this.dispatchSeq++}`;
27708
+ const correlationId = randomUUID();
27709
+ const meta = task.metadata ?? {};
27710
+ const model = (typeof meta.model === "string" ? meta.model : void 0) ?? this.opts.defaultModel;
27711
+ const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? this.opts.defaultProvider;
27712
+ const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
27713
+ const spawnResult = await coordinator.spawn({
27714
+ id: subagentId,
27715
+ name: agentName ?? subagentId,
27716
+ role: "executor",
27717
+ // Idle reaper is always on; the hard wall-clock cap only when opted in.
27718
+ idleTimeoutMs: this.idleTimeoutMs,
27719
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
27720
+ cwd: this.taskCwds.get(taskId),
27721
+ disabledTools: ["delegate"],
27722
+ ...model ? { model } : {},
27723
+ ...provider ? { provider } : {},
27724
+ ...fallbackModels && fallbackModels.length ? { fallbackModels } : {}
27725
+ });
27726
+ if (!spawnResult.subagentId) {
27727
+ throw new SddError({
27728
+ message: "One or more subagent spawns failed",
27729
+ code: ERROR_CODES.SDD_INVALID_STATE
27730
+ });
26945
27731
  }
26946
- const progress = computeTaskProgress(this.opts.graph);
26947
- const taskIds = tasks.map(() => randomUUID());
26948
- const subagentIds = tasks.map((_, i) => `sdd-wave${wave}-${i}`);
27732
+ this.taskSubagents.set(taskId, subagentId);
27733
+ this.emit("sdd.task.started", {
27734
+ runId: this.runId,
27735
+ taskId,
27736
+ subagentId,
27737
+ agentName: agentName ?? "",
27738
+ worktreeBranch: this.taskBranches.get(taskId)
27739
+ });
26949
27740
  const directivePreamble = [
26950
27741
  "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
26951
27742
  "",
26952
- `Wave ${wave + 1} of ~${Math.ceil(progress.total / this.slots)}`,
26953
27743
  `Graph: ${this.opts.graph.title}`,
26954
- `Parallel slots: ${tasks.length}`,
26955
27744
  "",
26956
27745
  "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
26957
27746
  "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
@@ -26959,91 +27748,297 @@ var SddParallelRun = class {
26959
27748
  "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
26960
27749
  "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
26961
27750
  ].join("\n");
26962
- if (!this.coordinator) throw new SddError({
26963
- message: "SDD parallel runner requires a coordinator",
26964
- code: ERROR_CODES.SDD_INVALID_STATE
26965
- });
26966
- const coordinator = this.coordinator;
26967
- const spawns = subagentIds.map(
26968
- (subagentId) => coordinator.spawn({
26969
- id: subagentId,
26970
- name: subagentId,
26971
- role: "executor",
26972
- timeoutMs: this.timeoutMs,
26973
- // Disable delegation subagents are leaf workers, not orchestrators
26974
- disabledTools: ["delegate"]
26975
- })
26976
- );
26977
- const spawnResults = await Promise.all(spawns);
26978
- if (!spawnResults.every((r) => Boolean(r.subagentId))) {
26979
- throw new SddError({
26980
- message: "One or more subagent spawns failed",
26981
- code: ERROR_CODES.SDD_INVALID_STATE
26982
- });
26983
- }
26984
- const assignPromises = tasks.map((task, i) => {
26985
- const spec = {
26986
- id: taskIds[i] ?? task.id,
26987
- description: [
26988
- directivePreamble,
26989
- "",
26990
- `\u2500\u2500 TASK ${i + 1}/${tasks.length} \u2500\u2500`,
26991
- `[${task.priority.toUpperCase()}] ${task.title}`,
26992
- "",
26993
- task.description
26994
- ].join("\n"),
26995
- subagentId: subagentIds[i] ?? spawnResults[i]?.subagentId ?? task.id,
26996
- timeoutMs: this.timeoutMs
26997
- };
26998
- return this.coordinator?.assign(spec);
27751
+ await coordinator.assign({
27752
+ id: correlationId,
27753
+ description: [
27754
+ directivePreamble,
27755
+ "",
27756
+ `\u2500\u2500 TASK \u2500\u2500`,
27757
+ `[${task.priority.toUpperCase()}] ${task.title}`,
27758
+ "",
27759
+ task.description
27760
+ ].join("\n"),
27761
+ subagentId,
27762
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
26999
27763
  });
27000
- await Promise.all(assignPromises);
27001
- let results;
27764
+ let result;
27002
27765
  try {
27003
- results = await coordinator.awaitTasks(taskIds);
27766
+ const got = await coordinator.awaitTasks([correlationId]);
27767
+ result = expectDefined(got[0]);
27004
27768
  } catch (err) {
27005
- results = taskIds.map((id) => ({
27006
- subagentId: "",
27007
- taskId: id,
27769
+ result = {
27770
+ subagentId,
27771
+ taskId: correlationId,
27008
27772
  status: "failed",
27009
27773
  error: { kind: "unknown", message: String(err), retryable: false },
27010
27774
  iterations: 0,
27011
27775
  toolCalls: 0,
27012
27776
  durationMs: 0
27013
- }));
27777
+ };
27014
27778
  }
27015
- const successCount = results.filter((r) => r.status === "success").length;
27016
- const failCount = results.length - successCount;
27017
- for (let i = 0; i < results.length; i++) {
27018
- const result = expectDefined(results[i]);
27019
- const taskId = expectDefined(tasks[i]).id;
27020
- if (result.status === "success") {
27779
+ this.taskSubagents.delete(taskId);
27780
+ if (this.cancelledTasks.has(taskId)) {
27781
+ await this.resolveWorktrees([task]);
27782
+ return { taskId, success: false, result };
27783
+ }
27784
+ let verificationFailReason;
27785
+ if (result.status === "success" && this.opts.verifyTask) {
27786
+ const cwd = this.taskCwds.get(taskId) ?? this.opts.projectRoot;
27787
+ try {
27788
+ const verdict = await this.opts.verifyTask({ task, result, cwd });
27789
+ if (!verdict.ok) {
27790
+ verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
27791
+ }
27792
+ } catch (err) {
27793
+ verificationFailReason = `verification error: ${String(err)}`;
27794
+ }
27795
+ if (verificationFailReason) {
27796
+ this.emit("sdd.task.verification_failed", {
27797
+ runId: this.runId,
27798
+ taskId,
27799
+ reason: verificationFailReason
27800
+ });
27801
+ }
27802
+ }
27803
+ let success = false;
27804
+ if (result.status === "success" && !verificationFailReason) {
27805
+ const merged = await this.integrateWorktree(task, result);
27806
+ if (merged.ok) {
27807
+ success = true;
27021
27808
  this.opts.tracker.updateNodeStatus(taskId, "completed");
27022
27809
  this.retryMap.delete(taskId);
27810
+ this.persistRetries(taskId, 0);
27811
+ this.emit("sdd.task.completed", {
27812
+ runId: this.runId,
27813
+ taskId,
27814
+ subagentId,
27815
+ durationMs: result.durationMs
27816
+ });
27817
+ } else if (merged.reason) {
27818
+ this.emit("sdd.task.verification_failed", {
27819
+ runId: this.runId,
27820
+ taskId,
27821
+ reason: merged.reason
27822
+ });
27823
+ await this.applyTaskFailure(taskId, subagentId, merged.reason);
27023
27824
  } else {
27024
- const errMsg = result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error";
27025
- const currentRetries = this.retryMap.get(taskId) ?? 0;
27026
- if (currentRetries < this.maxRetries) {
27027
- this.retryMap.set(taskId, currentRetries + 1);
27028
- this.opts.tracker.updateNodeStatus(
27029
- taskId,
27030
- "pending",
27031
- `Retry ${currentRetries + 1}/${this.maxRetries}: ${errMsg}`
27032
- );
27825
+ this.emit("sdd.task.conflict", {
27826
+ runId: this.runId,
27827
+ taskId,
27828
+ conflictFiles: merged.conflictFiles ?? []
27829
+ });
27830
+ const reason = `merge conflict${merged.conflictFiles?.length ? `: ${merged.conflictFiles.join(", ")}` : ""}`;
27831
+ await this.applyTaskFailure(taskId, subagentId, reason);
27832
+ }
27833
+ } else {
27834
+ const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
27835
+ await this.applyTaskFailure(taskId, subagentId, errMsg);
27836
+ await this.resolveWorktrees([task]);
27837
+ }
27838
+ return { taskId, success, result };
27839
+ }
27840
+ /**
27841
+ * Apply a task failure: retry (→ pending, bump retry count) while attempts
27842
+ * remain, else consult the optional supervisor (which can rescue via
27843
+ * retry/reassign/split), else terminal-fail (→ failed). Shared by the
27844
+ * worker-failure, verification-gate, and merge-conflict paths so all three
27845
+ * negotiate the same retry budget and emit the same events.
27846
+ */
27847
+ async applyTaskFailure(taskId, subagentId, errMsg) {
27848
+ const currentRetries = this.retryMap.get(taskId) ?? 0;
27849
+ if (currentRetries < this.maxRetries) {
27850
+ this.retryMap.set(taskId, currentRetries + 1);
27851
+ this.persistRetries(taskId, currentRetries + 1);
27852
+ this.opts.tracker.updateNodeStatus(
27853
+ taskId,
27854
+ "pending",
27855
+ `Retry ${currentRetries + 1}/${this.maxRetries}: ${errMsg}`
27856
+ );
27857
+ this.emit("sdd.task.retrying", {
27858
+ runId: this.runId,
27859
+ taskId,
27860
+ attempt: currentRetries + 1,
27861
+ maxRetries: this.maxRetries
27862
+ });
27863
+ return;
27864
+ }
27865
+ if (await this.trySupervisorRescue(taskId, errMsg)) return;
27866
+ this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
27867
+ this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId, error: errMsg });
27868
+ }
27869
+ /**
27870
+ * Consult `superviseFailure` for a task that has exhausted its retries.
27871
+ * Applies the verdict (retry / reassign+retry / split) and returns true when
27872
+ * the task was rescued (caller must NOT terminal-fail it). Bounded per task by
27873
+ * `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
27874
+ */
27875
+ async trySupervisorRescue(taskId, errMsg) {
27876
+ const supervise = this.opts.superviseFailure;
27877
+ if (!supervise) return false;
27878
+ const used = this.supervisorEscalations.get(taskId) ?? 0;
27879
+ if (used >= this.maxSupervisorEscalations) return false;
27880
+ const node = this.opts.tracker.getNode(taskId);
27881
+ if (!node) return false;
27882
+ let verdict;
27883
+ try {
27884
+ verdict = await supervise({ task: node, error: errMsg, attempts: used });
27885
+ } catch {
27886
+ return false;
27887
+ }
27888
+ if (!verdict || verdict.action === "fail") return false;
27889
+ this.supervisorEscalations.set(taskId, used + 1);
27890
+ const requeue = (reason) => {
27891
+ this.retryMap.delete(taskId);
27892
+ this.persistRetries(taskId, 0);
27893
+ this.opts.tracker.updateNodeStatus(taskId, "pending", reason);
27894
+ };
27895
+ if (verdict.action === "reassign") {
27896
+ this.setTaskModel(taskId, verdict.model, verdict.provider);
27897
+ requeue(`supervisor reassign: ${verdict.model ?? "default"}`);
27898
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "reassign" });
27899
+ return true;
27900
+ }
27901
+ if (verdict.action === "split") {
27902
+ const ids = this.splitTask(taskId, verdict.subtasks);
27903
+ if (ids.length === 0) return false;
27904
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "split" });
27905
+ return true;
27906
+ }
27907
+ requeue("supervisor retry");
27908
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "retry" });
27909
+ return true;
27910
+ }
27911
+ /**
27912
+ * Integrate a verified-successful task's worktree into the base branch.
27913
+ * Commits, squash-merges (optionally running `conflictResolver` first), and on
27914
+ * success releases the worktree. On an UNRESOLVED conflict it returns
27915
+ * `{ok:false}` with the conflicting files so the caller routes the task into
27916
+ * the failure path (a retry forks a fresh worktree off the now-advanced base,
27917
+ * which usually clears the conflict). No-op `{ok:true}` when worktrees are
27918
+ * disabled or none was allocated for this task. Never throws — a merge hiccup
27919
+ * degrades to a (retryable) failure rather than wedging the run.
27920
+ */
27921
+ async integrateWorktree(task, result) {
27922
+ const wt = this.opts.worktrees;
27923
+ if (!wt) return { ok: true };
27924
+ const handle = this.taskWorktrees.get(task.id);
27925
+ if (!handle) return { ok: true };
27926
+ try {
27927
+ await wt.commitAll(handle, `sdd(${task.title}): ${task.id}`);
27928
+ const baseShaBefore = await wt.baseHead(handle);
27929
+ const baseSha = this.opts.conflictResolver ? baseShaBefore : null;
27930
+ const res = await wt.merge(handle, {
27931
+ squash: true,
27932
+ ...this.opts.conflictResolver ? {
27933
+ resolve: (info) => this.opts.conflictResolver({ task, conflictFiles: info.conflictFiles, cwd: info.cwd })
27934
+ } : {}
27935
+ });
27936
+ if (res.ok) {
27937
+ if (res.resolved && this.opts.verifyTask && baseSha) {
27938
+ let regressed;
27939
+ try {
27940
+ const verdict = await this.opts.verifyTask({
27941
+ task,
27942
+ result: result ?? {},
27943
+ cwd: this.opts.projectRoot
27944
+ });
27945
+ if (!verdict.ok) regressed = verdict.reason ?? "verification failed after conflict resolution";
27946
+ } catch (err) {
27947
+ regressed = `verification error after conflict resolution: ${String(err)}`;
27948
+ }
27949
+ if (regressed) {
27950
+ await wt.revertBaseTo(handle, baseSha).catch(() => {
27951
+ });
27952
+ await wt.release(handle, { keep: false }).catch(() => {
27953
+ });
27954
+ this.forgetWorktree(task.id, { keepBranchLabel: true });
27955
+ return { ok: false, conflictFiles: [], reason: regressed };
27956
+ }
27957
+ }
27958
+ const baseShaAfter = await wt.baseHead(handle);
27959
+ if (baseShaAfter && baseShaAfter !== baseShaBefore) {
27960
+ this.mergedCommits.push({ taskId: task.id, sha: baseShaAfter, title: task.title });
27961
+ this.emit("sdd.task.merged", { runId: this.runId, taskId: task.id, sha: baseShaAfter });
27962
+ }
27963
+ await wt.release(handle, { keep: false });
27964
+ this.forgetWorktree(task.id);
27965
+ return { ok: true };
27966
+ }
27967
+ await wt.release(handle, { keep: false }).catch(() => {
27968
+ });
27969
+ this.forgetWorktree(task.id, { keepBranchLabel: true });
27970
+ return { ok: false, conflictFiles: res.conflictFiles ?? [] };
27971
+ } catch {
27972
+ this.forgetWorktree(task.id);
27973
+ return { ok: false, conflictFiles: [] };
27974
+ }
27975
+ }
27976
+ /** Allocate a fresh git worktree per task in the batch (no-op without a manager). */
27977
+ async allocateWorktrees(tasks) {
27978
+ const wt = this.opts.worktrees;
27979
+ if (!wt) return;
27980
+ for (const task of tasks) {
27981
+ if (this.taskWorktrees.has(task.id)) continue;
27982
+ try {
27983
+ const handle = await wt.allocate(`sdd-${task.id}`, {
27984
+ slugHint: task.title,
27985
+ ownerLabel: task.title
27986
+ });
27987
+ if (handle.status === "active") {
27988
+ this.taskWorktrees.set(task.id, handle);
27989
+ this.taskCwds.set(task.id, handle.dir);
27990
+ this.taskBranches.set(task.id, handle.branch);
27991
+ const node = this.opts.tracker.getNode(task.id);
27992
+ if (node) node.metadata = { ...node.metadata, worktreeBranch: handle.branch };
27993
+ }
27994
+ } catch {
27995
+ }
27996
+ }
27997
+ }
27998
+ /**
27999
+ * Resolve each task's worktree after its result is known. Serialized merges
28000
+ * (one at a time) keep the base branch consistent; the wave structure already
28001
+ * guarantees dependency order (a task's blockers merged in an earlier wave).
28002
+ */
28003
+ async resolveWorktrees(tasks) {
28004
+ const wt = this.opts.worktrees;
28005
+ if (!wt) return;
28006
+ for (const task of tasks) {
28007
+ const handle = this.taskWorktrees.get(task.id);
28008
+ if (!handle) continue;
28009
+ const node = this.opts.tracker.getNode(task.id);
28010
+ const status = node?.status;
28011
+ const cancelled = Boolean(node?.metadata?.cancelled);
28012
+ try {
28013
+ if (cancelled) {
28014
+ await wt.release(handle, { keep: false });
28015
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
28016
+ } else if (status === "completed") {
28017
+ await wt.commitAll(handle, `sdd(${task.title}): ${task.id}`);
28018
+ await wt.merge(handle, { squash: true });
28019
+ await wt.release(handle, { keep: false });
28020
+ this.forgetWorktree(task.id);
28021
+ } else if (status === "failed") {
28022
+ await wt.release(handle, { keep: false });
28023
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
27033
28024
  } else {
27034
- this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
28025
+ await wt.release(handle, { keep: false });
28026
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
27035
28027
  }
28028
+ } catch {
28029
+ this.forgetWorktree(task.id);
27036
28030
  }
27037
28031
  }
27038
- return {
27039
- wave,
27040
- batch,
27041
- results,
27042
- successCount,
27043
- failCount,
27044
- durationMs: Date.now() - waveStart,
27045
- stopRequested: this.stopRequested
27046
- };
28032
+ }
28033
+ forgetWorktree(taskId, opts = {}) {
28034
+ this.taskWorktrees.delete(taskId);
28035
+ this.taskCwds.delete(taskId);
28036
+ if (!opts.keepBranchLabel) this.taskBranches.delete(taskId);
28037
+ }
28038
+ /** Persist a task's retry count into node metadata (survives crash → resume). */
28039
+ persistRetries(taskId, retries) {
28040
+ const node = this.opts.tracker.getNode(taskId);
28041
+ if (node) node.metadata = { ...node.metadata, retries };
27047
28042
  }
27048
28043
  buildProgress() {
27049
28044
  const gp = this.opts.tracker.getProgress();
@@ -27062,145 +28057,1881 @@ var SddParallelRun = class {
27062
28057
  }
27063
28058
  };
27064
28059
 
27065
- // src/observability/metrics.ts
27066
- var RESERVOIR_SIZE = 1024;
27067
- function labelKey(labels) {
27068
- if (!labels) return "";
27069
- const keys = Object.keys(labels).sort();
27070
- return keys.map((k) => `${k}=${labels[k]}`).join(",");
27071
- }
27072
- function quantile(sorted, q) {
27073
- if (sorted.length === 0) return 0;
27074
- const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length));
27075
- return sorted[idx] ?? 0;
28060
+ // src/core/fallback-model.ts
28061
+ function visibleProviderModels(config, providerId, providerModels) {
28062
+ const entry = config.providers?.[providerId];
28063
+ return entry?.models !== void 0 ? [...entry.models] : providerModels;
27076
28064
  }
27077
- var InMemoryMetricsSink = class {
27078
- counters = /* @__PURE__ */ new Map();
27079
- gauges = /* @__PURE__ */ new Map();
27080
- histograms = /* @__PURE__ */ new Map();
27081
- counter(name, value = 1, labels) {
27082
- const series = this.getOrCreate(this.counters, name);
27083
- const key = labelKey(labels);
27084
- const state = series.get(key) ?? { value: 0 };
27085
- state.value += value;
27086
- series.set(key, state);
28065
+ function parseModelRef(ref) {
28066
+ const trimmed = ref.trim();
28067
+ const slash = trimmed.indexOf("/");
28068
+ if (slash !== -1) {
28069
+ return {
28070
+ provider: trimmed.slice(0, slash) || void 0,
28071
+ model: trimmed.slice(slash + 1).trim()
28072
+ };
27087
28073
  }
27088
- gauge(name, value, labels) {
27089
- const series = this.getOrCreate(this.gauges, name);
27090
- series.set(labelKey(labels), { value });
28074
+ const parts = trimmed.split(/\s+/);
28075
+ if (parts.length >= 2) {
28076
+ return { provider: parts[0], model: parts.slice(1).join(" ") };
27091
28077
  }
27092
- histogram(name, value, labels) {
27093
- const series = this.getOrCreate(this.histograms, name);
27094
- const key = labelKey(labels);
27095
- let state = series.get(key);
27096
- if (!state) {
27097
- state = { count: 0, sum: 0, min: value, max: value, samples: [] };
27098
- series.set(key, state);
27099
- }
27100
- state.count++;
27101
- state.sum += value;
27102
- if (value < state.min) state.min = value;
27103
- if (value > state.max) state.max = value;
27104
- if (state.samples.length < RESERVOIR_SIZE) {
27105
- state.samples.push(value);
27106
- } else {
27107
- const r = Math.floor(Math.random() * state.count);
27108
- if (r < RESERVOIR_SIZE) state.samples[r] = value;
27109
- }
28078
+ return { model: trimmed };
28079
+ }
28080
+ function shouldFallback(err) {
28081
+ if (err instanceof StreamHangError) {
28082
+ return 599;
27110
28083
  }
27111
- snapshot() {
27112
- const series = [];
27113
- for (const [name, byLabel] of this.counters) {
27114
- for (const [key, state] of byLabel) {
27115
- series.push({
27116
- name,
27117
- type: "counter",
27118
- labels: parseLabelKey(key),
27119
- values: { value: state.value }
27120
- });
27121
- }
27122
- }
27123
- for (const [name, byLabel] of this.gauges) {
27124
- for (const [key, state] of byLabel) {
27125
- series.push({
27126
- name,
27127
- type: "gauge",
27128
- labels: parseLabelKey(key),
27129
- values: { value: state.value }
27130
- });
28084
+ if (!(err instanceof ProviderError)) return null;
28085
+ const s = err.status;
28086
+ if (s === 0) return s;
28087
+ if (s === 429 || s === 529 || s >= 500) return s;
28088
+ return null;
28089
+ }
28090
+ function providerHasKey(entry) {
28091
+ if (!entry) return false;
28092
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
28093
+ if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
28094
+ if (Array.isArray(entry.envVars) && entry.envVars.some((v) => !!process.env[v])) return true;
28095
+ return false;
28096
+ }
28097
+ var SMART_DEFAULT_MAX = 4;
28098
+ function smartDefaultFallbackChain(config) {
28099
+ const leaderProvider = config.provider;
28100
+ const leaderModel = config.model;
28101
+ const providers = config.providers ?? {};
28102
+ const seen = /* @__PURE__ */ new Set();
28103
+ const sameProvider = [];
28104
+ const crossProvider = [];
28105
+ const ids = Object.keys(providers).sort(
28106
+ (a, b) => a === leaderProvider ? -1 : b === leaderProvider ? 1 : a.localeCompare(b)
28107
+ );
28108
+ for (const id of ids) {
28109
+ const entry = providers[id];
28110
+ if (!providerHasKey(entry)) continue;
28111
+ const models = visibleProviderModels(config, id, entry?.models ?? []);
28112
+ for (const model of models) {
28113
+ if (id === leaderProvider && model === leaderModel) continue;
28114
+ const ref = `${id}/${model}`;
28115
+ if (seen.has(ref)) continue;
28116
+ seen.add(ref);
28117
+ (id === leaderProvider ? sameProvider : crossProvider).push(ref);
28118
+ }
28119
+ }
28120
+ return [...sameProvider, ...crossProvider].slice(0, SMART_DEFAULT_MAX);
28121
+ }
28122
+ function effectiveFallbackChain(config) {
28123
+ const explicit = config.fallbackModels ?? [];
28124
+ const filteredExplicit = explicit.filter((ref) => {
28125
+ const parsed = parseModelRef(ref);
28126
+ if (!parsed.model) return false;
28127
+ const providerId = parsed.provider ?? config.provider;
28128
+ const entry = config.providers?.[providerId];
28129
+ if (!entry?.models) return true;
28130
+ return entry.models.includes(parsed.model);
28131
+ });
28132
+ if (filteredExplicit.length > 0) return filteredExplicit;
28133
+ if (config.fallbackAuto === false) return [];
28134
+ return smartDefaultFallbackChain(config);
28135
+ }
28136
+ function createFallbackModelExtension(deps) {
28137
+ let dirty = false;
28138
+ return {
28139
+ name: "fallback-model",
28140
+ beforeRun: async (ctx) => {
28141
+ if (!dirty) return;
28142
+ const cfg = deps.getConfig();
28143
+ try {
28144
+ ctx.provider = await deps.buildProvider(cfg.provider);
28145
+ ctx.model = cfg.model;
28146
+ await deps.onModelSwitch?.(cfg.provider, cfg.model);
28147
+ } catch (err) {
28148
+ deps.logger?.warn(
28149
+ `fallback-model: could not restore primary "${cfg.provider}/${cfg.model}": ${err instanceof Error ? err.message : String(err)}`
28150
+ );
27131
28151
  }
27132
- }
27133
- for (const [name, byLabel] of this.histograms) {
27134
- for (const [key, state] of byLabel) {
27135
- const sorted = [...state.samples].sort((a, b) => a - b);
27136
- series.push({
27137
- name,
27138
- type: "histogram",
27139
- labels: parseLabelKey(key),
27140
- values: {
27141
- count: state.count,
27142
- sum: state.sum,
27143
- min: state.min,
27144
- max: state.max,
27145
- p50: quantile(sorted, 0.5),
27146
- p95: quantile(sorted, 0.95),
27147
- p99: quantile(sorted, 0.99)
28152
+ dirty = false;
28153
+ },
28154
+ wrapProviderRunner: async (ctx, request, inner) => {
28155
+ try {
28156
+ return await inner(ctx, request);
28157
+ } catch (firstErr) {
28158
+ let lastErr = firstErr;
28159
+ const cfg = deps.getConfig();
28160
+ const chain = effectiveFallbackChain(cfg);
28161
+ for (const ref of chain) {
28162
+ const status = shouldFallback(lastErr);
28163
+ if (status === null) break;
28164
+ const parsed = parseModelRef(ref);
28165
+ if (!parsed.model) continue;
28166
+ const targetProviderId = parsed.provider ?? cfg.provider;
28167
+ const from = { providerId: ctx.provider.id, model: ctx.model };
28168
+ let nextProvider;
28169
+ try {
28170
+ nextProvider = await deps.buildProvider(targetProviderId);
28171
+ } catch (err) {
28172
+ deps.logger?.warn(
28173
+ `fallback-model: skipping "${ref}" \u2014 cannot build provider "${targetProviderId}": ${err instanceof Error ? err.message : String(err)}`
28174
+ );
28175
+ continue;
27148
28176
  }
27149
- });
28177
+ const providerSwitched = nextProvider.id !== from.providerId;
28178
+ ctx.provider = nextProvider;
28179
+ ctx.model = parsed.model;
28180
+ request.model = parsed.model;
28181
+ dirty = true;
28182
+ await deps.onModelSwitch?.(targetProviderId, parsed.model);
28183
+ deps.events.emit("provider.fallback", {
28184
+ from,
28185
+ to: { providerId: nextProvider.id, model: parsed.model },
28186
+ status,
28187
+ providerSwitched
28188
+ });
28189
+ try {
28190
+ return await inner(ctx, request);
28191
+ } catch (err) {
28192
+ lastErr = err;
28193
+ }
28194
+ }
28195
+ throw lastErr;
27150
28196
  }
27151
28197
  }
27152
- return { timestamp: Date.now(), series };
27153
- }
27154
- reset() {
27155
- this.counters.clear();
27156
- this.gauges.clear();
27157
- this.histograms.clear();
27158
- }
27159
- getOrCreate(bag, name) {
27160
- let series = bag.get(name);
27161
- if (!series) {
27162
- series = /* @__PURE__ */ new Map();
27163
- bag.set(name, series);
27164
- }
27165
- return series;
27166
- }
27167
- };
27168
- function parseLabelKey(key) {
27169
- if (!key) return {};
27170
- const labels = {};
27171
- for (const pair of key.split(",")) {
27172
- const eq = pair.indexOf("=");
27173
- if (eq > 0) labels[pair.slice(0, eq)] = pair.slice(eq + 1);
27174
- }
27175
- return labels;
28198
+ };
27176
28199
  }
27177
- var NoopMetricsSink = class {
27178
- counter() {
27179
- }
27180
- gauge() {
27181
- }
27182
- histogram() {
27183
- }
27184
- snapshot() {
27185
- return { timestamp: Date.now(), series: [] };
27186
- }
27187
- reset() {
28200
+
28201
+ // src/sdd/sdd-supervisor.ts
28202
+ var SddSupervisor = class {
28203
+ constructor(opts) {
28204
+ this.opts = opts;
27188
28205
  }
28206
+ opts;
28207
+ /**
28208
+ * Bind this as `SddParallelRunOptions.superviseFailure`. Returns a verdict the
28209
+ * run applies, or `undefined`/`{action:'fail'}` to let the task terminal-fail.
28210
+ */
28211
+ superviseFailure = async (info) => {
28212
+ const { task, error, attempts } = info;
28213
+ const canReassign = (this.opts.reassignModels?.length ?? 0) > 0;
28214
+ const canSplit = Boolean(this.opts.generateSubtasks);
28215
+ const decision = await this.opts.brain.decide({
28216
+ id: `sdd-supervisor-${task.id}-${attempts}`,
28217
+ source: "system",
28218
+ question: `SDD task "${task.title}" exhausted its retries. How should the run proceed?`,
28219
+ context: `Error: ${error}
28220
+ Supervisor rescues already used: ${attempts}`,
28221
+ options: [
28222
+ { id: "retry", label: "Retry the task as-is", recommended: true },
28223
+ ...canReassign ? [{ id: "reassign", label: "Reassign to a different model" }] : [],
28224
+ ...canSplit ? [{ id: "split", label: "Split into smaller sub-tasks" }] : [],
28225
+ { id: "fail", label: "Give up and mark the task failed" }
28226
+ ],
28227
+ // Higher risk once we've already rescued it once — pushes a wired LLM/human
28228
+ // toward a decisive verdict instead of looping retries.
28229
+ risk: attempts >= 1 ? "high" : "medium",
28230
+ // `continue` → policy answers in place (bounded retry, LLM never runs).
28231
+ // `ask_human` → policy escalates so the autonomous LLM layer can actually
28232
+ // pick reassign/split (see requestLlmVerdict's safety contract).
28233
+ fallback: this.opts.requestLlmVerdict ? "ask_human" : "continue"
28234
+ });
28235
+ if (decision.type === "deny") return { action: "fail" };
28236
+ if (decision.type !== "answer") return { action: "retry" };
28237
+ const choice = decision.optionId ?? "retry";
28238
+ if (choice === "fail") return { action: "fail" };
28239
+ if (choice === "reassign" && canReassign) {
28240
+ const models = this.opts.reassignModels;
28241
+ const ref = models[attempts % models.length];
28242
+ const parsed = ref ? parseModelRef(ref) : void 0;
28243
+ return { action: "reassign", model: parsed?.model, provider: parsed?.provider };
28244
+ }
28245
+ if (choice === "split" && this.opts.generateSubtasks) {
28246
+ const subtasks = await this.opts.generateSubtasks({ task, error }).catch(() => []);
28247
+ return subtasks.length ? { action: "split", subtasks } : { action: "retry" };
28248
+ }
28249
+ return { action: "retry" };
28250
+ };
27189
28251
  };
28252
+ function makeCommandVerifier(options = {}) {
28253
+ const metadataKey = options.metadataKey ?? "verificationCommand";
28254
+ const timeoutMs = options.timeoutMs ?? 18e4;
28255
+ return async function verifyTask(info) {
28256
+ const cmd = info.task.metadata?.[metadataKey];
28257
+ if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
28258
+ return await new Promise((resolve19) => {
28259
+ const child = spawn(cmd, { cwd: info.cwd, shell: true, windowsHide: true, stdio: "ignore" });
28260
+ const timer = setTimeout(() => {
28261
+ child.kill();
28262
+ resolve19({ ok: false, reason: `verification timed out: ${cmd}` });
28263
+ }, timeoutMs);
28264
+ child.on("exit", (code) => {
28265
+ clearTimeout(timer);
28266
+ resolve19(
28267
+ code === 0 ? { ok: true } : { ok: false, reason: `verification failed (exit ${code}): ${cmd}` }
28268
+ );
28269
+ });
28270
+ child.on("error", (err) => {
28271
+ clearTimeout(timer);
28272
+ resolve19({ ok: false, reason: `verification spawn error: ${String(err)}` });
28273
+ });
28274
+ });
28275
+ };
28276
+ }
27190
28277
 
27191
- // src/observability/health.ts
27192
- var SEVERITY = {
27193
- healthy: 0,
27194
- degraded: 1,
27195
- unhealthy: 2
27196
- };
27197
- var DefaultHealthRegistry = class {
27198
- checks = /* @__PURE__ */ new Map();
27199
- timeoutMs;
27200
- constructor(opts = {}) {
27201
- this.timeoutMs = opts.timeoutMs ?? 5e3;
27202
- }
27203
- register(check) {
28278
+ // src/sdd/decompose-task.ts
28279
+ var TASK_TYPES = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
28280
+ var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
28281
+ function extractJsonArray(text) {
28282
+ const fence = text.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
28283
+ if (fence?.[1]) return fence[1].trim();
28284
+ const bare = text.match(/(\[[\s\S]*\])/);
28285
+ if (bare?.[1]) {
28286
+ try {
28287
+ if (Array.isArray(JSON.parse(bare[1]))) return bare[1];
28288
+ } catch {
28289
+ }
28290
+ }
28291
+ return null;
28292
+ }
28293
+ function buildPrompt(task, error, min, max) {
28294
+ return [
28295
+ "You are an engineering lead triaging a software task that FAILED after every",
28296
+ "automated retry was exhausted. Break it into smaller, independently-executable",
28297
+ `sub-tasks (between ${min} and ${max}) so separate workers can each tackle a`,
28298
+ "narrower slice. Each sub-task must be strictly smaller than the parent \u2014 never",
28299
+ "restate the whole task as one sub-task.",
28300
+ "",
28301
+ `Parent task title: ${task.title}`,
28302
+ `Parent description: ${task.description}`,
28303
+ `Failure / error: ${error || "(none recorded)"}`,
28304
+ "",
28305
+ "Respond with ONLY a JSON array (no prose) of objects with this shape:",
28306
+ '[{"title": "...", "description": "...", "type": "feature|bugfix|refactor|docs|test|chore", "priority": "critical|high|medium|low"}]',
28307
+ "`type` and `priority` are optional (they default to the parent's)."
28308
+ ].join("\n");
28309
+ }
28310
+ function makeLlmSubtaskGenerator(opts) {
28311
+ const min = Math.max(2, opts.minSubtasks ?? 2);
28312
+ const max = Math.max(min, opts.maxSubtasks ?? 4);
28313
+ return async function generateSubtasks(info) {
28314
+ let text;
28315
+ try {
28316
+ text = await opts.run(buildPrompt(info.task, info.error, min, max));
28317
+ } catch {
28318
+ return [];
28319
+ }
28320
+ const json = extractJsonArray(text ?? "");
28321
+ if (!json) return [];
28322
+ let raw;
28323
+ try {
28324
+ raw = JSON.parse(json);
28325
+ } catch {
28326
+ return [];
28327
+ }
28328
+ if (!Array.isArray(raw)) return [];
28329
+ const specs = [];
28330
+ for (const item of raw) {
28331
+ if (!item || typeof item !== "object") continue;
28332
+ const r = item;
28333
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
28334
+ const description = typeof r["description"] === "string" ? r["description"].trim() : "";
28335
+ if (!title || !description) continue;
28336
+ const type = TASK_TYPES.has(r["type"]) ? r["type"] : void 0;
28337
+ const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
28338
+ specs.push({ title, description, type, priority });
28339
+ if (specs.length >= max) break;
28340
+ }
28341
+ return specs.length >= min ? specs : [];
28342
+ };
28343
+ }
28344
+ var START = "<<<<<<<";
28345
+ var BASE = "|||||||";
28346
+ var SEP2 = "=======";
28347
+ var END = ">>>>>>>";
28348
+ function resolveConflictText(text, side) {
28349
+ const out = [];
28350
+ let state = "normal";
28351
+ for (const line of text.split("\n")) {
28352
+ const marker = line.slice(0, 7);
28353
+ if (state === "normal" && marker === START) {
28354
+ state = "ours";
28355
+ continue;
28356
+ }
28357
+ if (state !== "normal" && marker === BASE) {
28358
+ state = "base";
28359
+ continue;
28360
+ }
28361
+ if (state !== "normal" && marker === SEP2) {
28362
+ state = "theirs";
28363
+ continue;
28364
+ }
28365
+ if (state !== "normal" && marker === END) {
28366
+ state = "normal";
28367
+ continue;
28368
+ }
28369
+ if (state === "normal") out.push(line);
28370
+ else if (state === "ours" && side === "base") out.push(line);
28371
+ else if (state === "theirs" && side === "incoming") out.push(line);
28372
+ }
28373
+ return out.join("\n");
28374
+ }
28375
+ function hasConflictMarkers(text) {
28376
+ return text.split("\n").some((l) => {
28377
+ const m = l.slice(0, 7);
28378
+ return m === START || m === SEP2 || m === END || m === BASE;
28379
+ });
28380
+ }
28381
+ function makePreferSideConflictResolver(side) {
28382
+ return async function conflictResolver(info) {
28383
+ if (info.conflictFiles.length === 0) return false;
28384
+ for (const rel of info.conflictFiles) {
28385
+ const abs = isAbsolute(rel) ? rel : join(info.cwd, rel);
28386
+ let content;
28387
+ try {
28388
+ content = await readFile(abs, "utf8");
28389
+ } catch {
28390
+ return false;
28391
+ }
28392
+ const resolved = resolveConflictText(content, side);
28393
+ if (hasConflictMarkers(resolved)) return false;
28394
+ try {
28395
+ await writeFile(abs, resolved, "utf8");
28396
+ } catch {
28397
+ return false;
28398
+ }
28399
+ }
28400
+ return true;
28401
+ };
28402
+ }
28403
+ function unfence(text) {
28404
+ const m = text.match(/^[\s\S]*?```[^\n]*\n([\s\S]*?)\n```[\s\S]*$/);
28405
+ return m?.[1] !== void 0 ? m[1] : text.trim();
28406
+ }
28407
+ function nonMarkerLineCount(text) {
28408
+ return text.split("\n").filter((l) => {
28409
+ const m = l.slice(0, 7);
28410
+ return m !== START && m !== SEP2 && m !== END && m !== BASE;
28411
+ }).length;
28412
+ }
28413
+ function makeLlmConflictResolver(opts) {
28414
+ const minFraction = opts.minRetainedFraction ?? 0.5;
28415
+ return async function conflictResolver(info) {
28416
+ if (info.conflictFiles.length === 0) return false;
28417
+ for (const rel of info.conflictFiles) {
28418
+ const abs = isAbsolute(rel) ? rel : join(info.cwd, rel);
28419
+ let content;
28420
+ try {
28421
+ content = await readFile(abs, "utf8");
28422
+ } catch {
28423
+ return false;
28424
+ }
28425
+ if (!hasConflictMarkers(content)) continue;
28426
+ const prompt = [
28427
+ "You are resolving a git MERGE CONFLICT in a single file. Below is the",
28428
+ "full file with conflict markers (<<<<<<<, =======, >>>>>>>, and possibly",
28429
+ "||||||| for diff3). Combine both sides into the correct, complete file \u2014",
28430
+ "keep ALL non-conflicting content verbatim and reconcile each hunk sensibly.",
28431
+ "Return ONLY the fully resolved file contents (no conflict markers, no",
28432
+ "commentary), optionally wrapped in a single ``` code fence.",
28433
+ "",
28434
+ `File: ${rel}`,
28435
+ "--- BEGIN ---",
28436
+ content,
28437
+ "--- END ---"
28438
+ ].join("\n");
28439
+ let out;
28440
+ try {
28441
+ out = await opts.run(prompt);
28442
+ } catch {
28443
+ return false;
28444
+ }
28445
+ const resolved = unfence(out ?? "");
28446
+ if (!resolved.trim() || hasConflictMarkers(resolved)) return false;
28447
+ if (resolved.split("\n").length < Math.floor(nonMarkerLineCount(content) * minFraction)) {
28448
+ return false;
28449
+ }
28450
+ try {
28451
+ await writeFile(abs, resolved, "utf8");
28452
+ } catch {
28453
+ return false;
28454
+ }
28455
+ }
28456
+ return true;
28457
+ };
28458
+ }
28459
+
28460
+ // src/sdd/board-types.ts
28461
+ function shortIdMap(graph) {
28462
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
28463
+ const m = /* @__PURE__ */ new Map();
28464
+ nodes.forEach((n, i) => {
28465
+ m.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
28466
+ });
28467
+ return m;
28468
+ }
28469
+ function buildBoardTasks(graph) {
28470
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
28471
+ const shortId = shortIdMap(graph);
28472
+ const blockers = /* @__PURE__ */ new Map();
28473
+ for (const n of nodes) blockers.set(n.id, []);
28474
+ for (const e of graph.edges) {
28475
+ if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
28476
+ }
28477
+ const statusOf = (id) => graph.nodes.get(id)?.status;
28478
+ const depthCache = /* @__PURE__ */ new Map();
28479
+ const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
28480
+ const cached = depthCache.get(id);
28481
+ if (cached !== void 0) return cached;
28482
+ if (seen.has(id)) return 0;
28483
+ seen.add(id);
28484
+ const deps = blockers.get(id) ?? [];
28485
+ const d = deps.length === 0 ? 0 : 1 + Math.max(...deps.map((b) => depthOf(b, seen)));
28486
+ depthCache.set(id, d);
28487
+ return d;
28488
+ };
28489
+ const toTask = (n) => {
28490
+ const deps = blockers.get(n.id) ?? [];
28491
+ const allDepsDone = deps.every((b) => statusOf(b) === "completed");
28492
+ const meta = n.metadata ?? {};
28493
+ const cancelled = Boolean(meta["cancelled"]);
28494
+ const displayStatus = cancelled ? "cancelled" : n.status === "pending" && deps.length > 0 && allDepsDone ? "queued" : n.status;
28495
+ return {
28496
+ id: n.id,
28497
+ shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
28498
+ title: n.title,
28499
+ description: n.description,
28500
+ status: n.status,
28501
+ displayStatus,
28502
+ priority: n.priority,
28503
+ type: n.type,
28504
+ deps: deps.map((b) => shortId.get(b) ?? b.slice(0, 6)),
28505
+ agentName: n.assignee,
28506
+ worktreeBranch: typeof meta["worktreeBranch"] === "string" ? meta["worktreeBranch"] : void 0,
28507
+ startedAt: n.startedAt,
28508
+ completedAt: n.completedAt,
28509
+ retries: typeof meta["retries"] === "number" ? meta["retries"] : 0,
28510
+ model: typeof meta["model"] === "string" ? meta["model"] : void 0,
28511
+ provider: typeof meta["provider"] === "string" ? meta["provider"] : void 0,
28512
+ fallbackModels: Array.isArray(meta["fallbackModels"]) ? meta["fallbackModels"] : void 0,
28513
+ verificationCommand: typeof meta["verificationCommand"] === "string" ? meta["verificationCommand"] : void 0
28514
+ };
28515
+ };
28516
+ const tasks = nodes.map(toTask);
28517
+ const byDepth = /* @__PURE__ */ new Map();
28518
+ for (const n of nodes) {
28519
+ const d = depthOf(n.id);
28520
+ if (!byDepth.has(d)) byDepth.set(d, []);
28521
+ byDepth.get(d)?.push(shortId.get(n.id) ?? n.id.slice(0, 6));
28522
+ }
28523
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, taskIds: byDepth.get(d) ?? [] }));
28524
+ return { tasks, columns };
28525
+ }
28526
+ function buildBoardSnapshot(graph, run, now) {
28527
+ const { tasks, columns } = buildBoardTasks(graph);
28528
+ return {
28529
+ runId: run.runId,
28530
+ specId: run.specId,
28531
+ graphId: graph.id,
28532
+ title: graph.title,
28533
+ status: run.status,
28534
+ startedAt: run.startedAt,
28535
+ updatedAt: now,
28536
+ progress: computeTaskProgress(graph),
28537
+ wave: run.wave,
28538
+ tasks,
28539
+ columns,
28540
+ diagnostics: run.deadlockChains?.length ? { deadlockChains: run.deadlockChains } : void 0,
28541
+ defaultModel: run.defaultModel,
28542
+ defaultProvider: run.defaultProvider,
28543
+ fallbackModels: run.fallbackModels,
28544
+ baseBranch: run.baseBranch,
28545
+ mergedCommits: run.mergedCommits?.length ? run.mergedCommits : void 0
28546
+ };
28547
+ }
28548
+
28549
+ // src/sdd/sdd-board-store.ts
28550
+ init_atomic_write();
28551
+ var SddBoardStore = class {
28552
+ baseDir;
28553
+ indexPath;
28554
+ constructor(opts) {
28555
+ this.baseDir = opts.baseDir;
28556
+ this.indexPath = path3.join(this.baseDir, "_index.json");
28557
+ }
28558
+ snapshotPath(runId) {
28559
+ return path3.join(this.baseDir, `${this.safe(runId)}.json`);
28560
+ }
28561
+ eventsPath(runId) {
28562
+ return path3.join(this.baseDir, `${this.safe(runId)}.events.jsonl`);
28563
+ }
28564
+ controlPath(runId) {
28565
+ return path3.join(this.baseDir, `${this.safe(runId)}.control.jsonl`);
28566
+ }
28567
+ async saveSnapshot(snapshot) {
28568
+ await ensureDir(this.baseDir);
28569
+ await atomicWrite(this.snapshotPath(snapshot.runId), JSON.stringify(snapshot, null, 2), {
28570
+ mode: 384
28571
+ });
28572
+ await this.updateIndex(snapshot);
28573
+ }
28574
+ async load(runId) {
28575
+ try {
28576
+ const raw = await fsp3.readFile(this.snapshotPath(runId), "utf8");
28577
+ return JSON.parse(raw);
28578
+ } catch {
28579
+ return null;
28580
+ }
28581
+ }
28582
+ async list() {
28583
+ const index = await this.readIndex();
28584
+ return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
28585
+ }
28586
+ async loadLatestForSpec(specId) {
28587
+ const entry = (await this.list()).find((e) => e.specId === specId);
28588
+ return entry ? this.load(entry.runId) : null;
28589
+ }
28590
+ /** Append one line to the board's JSONL event log (best-effort, never throws). */
28591
+ async appendEvent(runId, event) {
28592
+ try {
28593
+ await ensureDir(this.baseDir);
28594
+ await fsp3.appendFile(this.eventsPath(runId), `${JSON.stringify(event)}
28595
+ `, { mode: 384 });
28596
+ } catch {
28597
+ }
28598
+ }
28599
+ /** Append a control command (used by readers to steer a CLI-owned run). */
28600
+ async appendControl(runId, command) {
28601
+ await ensureDir(this.baseDir);
28602
+ await fsp3.appendFile(this.controlPath(runId), `${JSON.stringify(command)}
28603
+ `, { mode: 384 });
28604
+ }
28605
+ /** Read + truncate the control queue (the run drains it). Returns parsed commands. */
28606
+ async drainControl(runId) {
28607
+ const p = this.controlPath(runId);
28608
+ let raw;
28609
+ try {
28610
+ raw = await fsp3.readFile(p, "utf8");
28611
+ } catch {
28612
+ return [];
28613
+ }
28614
+ try {
28615
+ await fsp3.writeFile(p, "", { mode: 384 });
28616
+ } catch {
28617
+ }
28618
+ return raw.split("\n").filter((l) => l.trim()).map((l) => {
28619
+ try {
28620
+ return JSON.parse(l);
28621
+ } catch {
28622
+ return null;
28623
+ }
28624
+ }).filter((c) => c !== null);
28625
+ }
28626
+ async delete(runId) {
28627
+ await Promise.allSettled([
28628
+ fsp3.unlink(this.snapshotPath(runId)),
28629
+ fsp3.unlink(this.eventsPath(runId)),
28630
+ fsp3.unlink(this.controlPath(runId))
28631
+ ]);
28632
+ await this.removeFromIndex(runId);
28633
+ }
28634
+ // ── internal ────────────────────────────────────────────────────────────
28635
+ safe(runId) {
28636
+ return runId.replace(/[^a-zA-Z0-9._-]/g, "_");
28637
+ }
28638
+ async readIndex() {
28639
+ try {
28640
+ const raw = await fsp3.readFile(this.indexPath, "utf8");
28641
+ const parsed = JSON.parse(raw);
28642
+ if (parsed?.version === 1) return parsed;
28643
+ } catch {
28644
+ }
28645
+ return { version: 1, entries: [] };
28646
+ }
28647
+ async updateIndex(snapshot) {
28648
+ const index = await this.readIndex();
28649
+ const entry = {
28650
+ runId: snapshot.runId,
28651
+ specId: snapshot.specId,
28652
+ title: snapshot.title,
28653
+ status: snapshot.status,
28654
+ total: snapshot.progress.total,
28655
+ completed: snapshot.progress.completed,
28656
+ updatedAt: snapshot.updatedAt
28657
+ };
28658
+ const idx = index.entries.findIndex((e) => e.runId === snapshot.runId);
28659
+ if (idx >= 0) index.entries[idx] = entry;
28660
+ else index.entries.push(entry);
28661
+ await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
28662
+ }
28663
+ async removeFromIndex(runId) {
28664
+ const index = await this.readIndex();
28665
+ index.entries = index.entries.filter((e) => e.runId !== runId);
28666
+ await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
28667
+ }
28668
+ };
28669
+
28670
+ // src/sdd/sdd-board-projector.ts
28671
+ var SddBoardProjector = class _SddBoardProjector {
28672
+ o;
28673
+ now;
28674
+ throttleMs;
28675
+ shortId;
28676
+ status = "idle";
28677
+ wave = 0;
28678
+ startedAt;
28679
+ deadlockChains = [];
28680
+ /** Live activity feed, most recent first (capped). */
28681
+ feed = [];
28682
+ static FEED_CAP = 60;
28683
+ finished = false;
28684
+ runDeadlocked = false;
28685
+ runStopped = false;
28686
+ /** Squash commits the run landed on the base branch (for post-run rollback). */
28687
+ mergedCommits = [];
28688
+ /** Base branch reported by the run at start (overrides the constructor option). */
28689
+ runBaseBranch;
28690
+ dirty = false;
28691
+ timer = null;
28692
+ unsubs = [];
28693
+ /** Tail of in-flight persistence, so callers can await a settled state. */
28694
+ lastSave = Promise.resolve();
28695
+ constructor(opts) {
28696
+ this.o = opts;
28697
+ this.now = opts.now ?? Date.now;
28698
+ this.throttleMs = opts.throttleMs ?? 250;
28699
+ this.shortId = shortIdMap(opts.graph);
28700
+ this.startedAt = this.now();
28701
+ this.unsubs.push(opts.tracker.subscribe(() => this.markDirty()));
28702
+ this.onRun("sdd.run.started", (e) => {
28703
+ this.status = "running";
28704
+ this.startedAt = this.now();
28705
+ if (e.baseBranch) this.runBaseBranch = e.baseBranch;
28706
+ this.markDirty();
28707
+ });
28708
+ this.onRun("sdd.run.finished", (e) => {
28709
+ this.finished = true;
28710
+ this.runDeadlocked = e.deadlocked;
28711
+ this.runStopped = e.stopped;
28712
+ this.flush();
28713
+ });
28714
+ this.onRun("sdd.wave", (e) => {
28715
+ this.wave = e.wave;
28716
+ this.pushFeed({ ts: this.now(), kind: "wave", text: `Wave ${e.wave + 1} started \xB7 ${e.batchSize} task(s) in parallel` });
28717
+ this.markDirty();
28718
+ });
28719
+ this.onRun("sdd.deadlock", (e) => {
28720
+ this.deadlockChains = e.chains.map((c) => ({
28721
+ blocked: this.shortId.get(c.blocked) ?? c.blocked.slice(0, 6),
28722
+ blockedBy: c.blockedBy.map((b) => this.shortId.get(b) ?? b.slice(0, 6))
28723
+ }));
28724
+ this.pushFeed({ ts: this.now(), kind: "deadlock", text: `Deadlock \u2014 ${e.chains.length} task(s) blocked by failed work` });
28725
+ this.markDirty();
28726
+ });
28727
+ this.onRun("sdd.task.started", (e) => {
28728
+ const sid = this.shortId.get(e.taskId);
28729
+ this.pushFeed({
28730
+ ts: this.now(),
28731
+ kind: "started",
28732
+ taskShortId: sid,
28733
+ agentName: e.agentName,
28734
+ text: `${e.agentName || "a worker"} picked up ${sid ?? "a task"}${this.titleOf(e.taskId)}`
28735
+ });
28736
+ this.markDirty();
28737
+ });
28738
+ this.onRun("sdd.task.completed", (e) => {
28739
+ const sid = this.shortId.get(e.taskId);
28740
+ const agent = this.assigneeOf(e.taskId);
28741
+ this.pushFeed({
28742
+ ts: this.now(),
28743
+ kind: "completed",
28744
+ taskShortId: sid,
28745
+ agentName: agent,
28746
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} completed${agent ? ` by ${agent}` : ""} \xB7 ${(e.durationMs / 1e3).toFixed(1)}s`
28747
+ });
28748
+ this.markDirty();
28749
+ });
28750
+ this.onRun("sdd.task.failed", (e) => {
28751
+ const sid = this.shortId.get(e.taskId);
28752
+ this.pushFeed({
28753
+ ts: this.now(),
28754
+ kind: "failed",
28755
+ taskShortId: sid,
28756
+ agentName: this.assigneeOf(e.taskId),
28757
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} failed \u2014 ${e.error}`
28758
+ });
28759
+ this.markDirty();
28760
+ });
28761
+ this.onRun("sdd.task.retrying", (e) => {
28762
+ const sid = this.shortId.get(e.taskId);
28763
+ this.pushFeed({
28764
+ ts: this.now(),
28765
+ kind: "retrying",
28766
+ taskShortId: sid,
28767
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} retrying (${e.attempt}/${e.maxRetries})`
28768
+ });
28769
+ this.markDirty();
28770
+ });
28771
+ this.onRun("sdd.task.verification_failed", (e) => {
28772
+ const sid = this.shortId.get(e.taskId);
28773
+ this.pushFeed({
28774
+ ts: this.now(),
28775
+ kind: "verification_failed",
28776
+ taskShortId: sid,
28777
+ agentName: this.assigneeOf(e.taskId),
28778
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} failed verification \u2014 ${e.reason}`
28779
+ });
28780
+ this.markDirty();
28781
+ });
28782
+ this.onRun("sdd.task.conflict", (e) => {
28783
+ const sid = this.shortId.get(e.taskId);
28784
+ const files = e.conflictFiles.length;
28785
+ this.pushFeed({
28786
+ ts: this.now(),
28787
+ kind: "conflict",
28788
+ taskShortId: sid,
28789
+ agentName: this.assigneeOf(e.taskId),
28790
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} merge conflict \u2014 ${files} file(s)${files ? `: ${e.conflictFiles.slice(0, 3).join(", ")}${files > 3 ? "\u2026" : ""}` : ""}`
28791
+ });
28792
+ this.markDirty();
28793
+ });
28794
+ this.onRun("sdd.task.merged", (e) => {
28795
+ const title = this.o.graph.nodes.get(e.taskId)?.title ?? "";
28796
+ this.mergedCommits.push({ taskId: e.taskId, sha: e.sha, title });
28797
+ const sid = this.shortId.get(e.taskId);
28798
+ this.pushFeed({
28799
+ ts: this.now(),
28800
+ kind: "completed",
28801
+ taskShortId: sid,
28802
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} merged \u2192 ${this.runBaseBranch ?? this.o.baseBranch ?? "base"} (${e.sha.slice(0, 8)})`
28803
+ });
28804
+ this.markDirty();
28805
+ });
28806
+ this.onRun("sdd.task.split", (e) => {
28807
+ const sid = this.shortId.get(e.taskId);
28808
+ this.pushFeed({
28809
+ ts: this.now(),
28810
+ kind: "split",
28811
+ taskShortId: sid,
28812
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} split into ${e.subtaskIds.length} sub-task(s)`
28813
+ });
28814
+ this.markDirty();
28815
+ });
28816
+ this.onRun("sdd.supervisor.decision", (e) => {
28817
+ const sid = this.shortId.get(e.taskId);
28818
+ this.pushFeed({
28819
+ ts: this.now(),
28820
+ kind: "supervisor",
28821
+ taskShortId: sid,
28822
+ text: `supervisor \u2192 ${e.action} for ${sid ?? "task"}${this.titleOf(e.taskId)}${e.rationale ? ` (${e.rationale})` : ""}`
28823
+ });
28824
+ this.markDirty();
28825
+ });
28826
+ }
28827
+ pushFeed(entry) {
28828
+ this.feed.unshift(entry);
28829
+ if (this.feed.length > _SddBoardProjector.FEED_CAP) this.feed.length = _SddBoardProjector.FEED_CAP;
28830
+ }
28831
+ /** ` (title…)` suffix for a feed line, or '' when the node/title is missing. */
28832
+ titleOf(taskId) {
28833
+ const t2 = this.o.graph.nodes.get(taskId)?.title;
28834
+ if (!t2) return "";
28835
+ return ` (${t2.length > 40 ? `${t2.slice(0, 39)}\u2026` : t2})`;
28836
+ }
28837
+ assigneeOf(taskId) {
28838
+ return this.o.graph.nodes.get(taskId)?.assignee;
28839
+ }
28840
+ /** Latest snapshot, built on demand (e.g. for a late-joining client). */
28841
+ snapshot() {
28842
+ return this.build();
28843
+ }
28844
+ /** Resolve once all in-flight snapshot persistence has settled. */
28845
+ async drain() {
28846
+ await this.lastSave;
28847
+ }
28848
+ /** Stop projecting and release subscriptions. */
28849
+ dispose() {
28850
+ if (this.timer) {
28851
+ clearTimeout(this.timer);
28852
+ this.timer = null;
28853
+ }
28854
+ for (const u of this.unsubs) u();
28855
+ this.unsubs.length = 0;
28856
+ }
28857
+ // ── internal ────────────────────────────────────────────────────────────
28858
+ /** Subscribe to a run event scoped to this run id; also append to JSONL. */
28859
+ onRun(event, handler) {
28860
+ const wrapped = (e) => {
28861
+ if (e.runId !== this.o.runId) return;
28862
+ void this.o.store?.appendEvent(this.o.runId, { ts: this.now(), type: event, payload: e });
28863
+ handler(e);
28864
+ };
28865
+ const off = this.o.events.on(event, wrapped);
28866
+ this.unsubs.push(off);
28867
+ }
28868
+ resolveStatus(completed, total) {
28869
+ if (!this.finished) return this.status;
28870
+ if (this.runDeadlocked) return "deadlocked";
28871
+ if (total > 0 && completed >= total) return "completed";
28872
+ if (this.runStopped) return "paused";
28873
+ return "failed";
28874
+ }
28875
+ build() {
28876
+ const snap = buildBoardSnapshot(
28877
+ this.o.graph,
28878
+ {
28879
+ runId: this.o.runId,
28880
+ specId: this.o.specId,
28881
+ status: "running",
28882
+ startedAt: this.startedAt,
28883
+ wave: this.wave,
28884
+ deadlockChains: this.deadlockChains,
28885
+ defaultModel: this.o.defaultModel,
28886
+ defaultProvider: this.o.defaultProvider,
28887
+ fallbackModels: this.o.fallbackModels,
28888
+ baseBranch: this.runBaseBranch ?? this.o.baseBranch,
28889
+ mergedCommits: this.mergedCommits
28890
+ },
28891
+ this.now()
28892
+ );
28893
+ snap.status = this.resolveStatus(snap.progress.completed, snap.progress.total);
28894
+ snap.feed = this.feed.slice(0, _SddBoardProjector.FEED_CAP);
28895
+ return snap;
28896
+ }
28897
+ markDirty() {
28898
+ this.dirty = true;
28899
+ if (this.timer || this.finished) return;
28900
+ this.timer = setTimeout(() => {
28901
+ this.timer = null;
28902
+ if (this.dirty) this.flush();
28903
+ }, this.throttleMs);
28904
+ }
28905
+ flush() {
28906
+ this.dirty = false;
28907
+ if (this.timer) {
28908
+ clearTimeout(this.timer);
28909
+ this.timer = null;
28910
+ }
28911
+ const snap = this.build();
28912
+ this.o.events.emit("sdd.board.snapshot", { runId: this.o.runId, snapshot: snap });
28913
+ if (this.o.store) {
28914
+ const store = this.o.store;
28915
+ this.lastSave = this.lastSave.then(() => store.saveSnapshot(snap)).catch(() => {
28916
+ });
28917
+ }
28918
+ }
28919
+ };
28920
+
28921
+ // src/sdd/sdd-run-registry.ts
28922
+ var SddRunRegistry = class {
28923
+ current = null;
28924
+ register(control) {
28925
+ this.current = control;
28926
+ }
28927
+ clear(runId) {
28928
+ if (this.current?.runId === runId) this.current = null;
28929
+ }
28930
+ getActive() {
28931
+ return this.current;
28932
+ }
28933
+ };
28934
+
28935
+ // src/sdd/sdd-interview-driver.ts
28936
+ var SddInterviewDriver = class {
28937
+ builder;
28938
+ o;
28939
+ minQuestions;
28940
+ maxQuestions;
28941
+ tracker = null;
28942
+ graph = null;
28943
+ constructor(opts) {
28944
+ this.o = opts;
28945
+ this.minQuestions = opts.minQuestions ?? 2;
28946
+ this.maxQuestions = opts.maxQuestions ?? 10;
28947
+ this.builder = new AISpecBuilder({
28948
+ store: opts.specStore,
28949
+ sessionPath: opts.sessionPath,
28950
+ projectContext: opts.projectContext,
28951
+ minQuestions: this.minQuestions,
28952
+ maxQuestions: this.maxQuestions
28953
+ });
28954
+ }
28955
+ /** Begin a fresh interview. Returns the first AI prompt (a question kickoff). */
28956
+ start(title, intent) {
28957
+ this.builder.startSession(title, intent);
28958
+ this.tracker = null;
28959
+ this.graph = null;
28960
+ return this.builder.getAIPrompt();
28961
+ }
28962
+ /**
28963
+ * Resume a previously-persisted interview from disk. Re-hydrates the task
28964
+ * graph too when one was already produced. Returns true if a session loaded.
28965
+ */
28966
+ async loadExisting() {
28967
+ const loaded = await this.builder.loadSession();
28968
+ if (!loaded) return false;
28969
+ const graphId = this.builder.getTaskGraphId();
28970
+ if (graphId) {
28971
+ const graph = await this.o.graphStore.load(graphId);
28972
+ if (graph) {
28973
+ this.graph = graph;
28974
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
28975
+ tracker.setGraph(graph);
28976
+ this.tracker = tracker;
28977
+ }
28978
+ }
28979
+ return true;
28980
+ }
28981
+ phase() {
28982
+ return this.builder.getPhase();
28983
+ }
28984
+ currentPrompt() {
28985
+ return this.builder.getAIPrompt();
28986
+ }
28987
+ getTracker() {
28988
+ return this.tracker;
28989
+ }
28990
+ getGraph() {
28991
+ return this.graph;
28992
+ }
28993
+ /** Record a Q/A pair (the agent asked `question`, the user replied `answer`). */
28994
+ submitAnswer(question, answer) {
28995
+ this.builder.addAnswer(question, answer);
28996
+ }
28997
+ /**
28998
+ * Feed the agent's text output back into the interview. Detects, in order:
28999
+ * 1. a Specification JSON → setSpec (phase → spec_review) + persist to SpecStore
29000
+ * 2. an implementation plan (implementation phase) → setImplementation
29001
+ * 3. a task JSON array → build + persist a TaskGraph
29002
+ * Each step is independent and best-effort; a malformed payload is ignored
29003
+ * rather than thrown, so a chatty agent turn never breaks the interview.
29004
+ */
29005
+ async ingestAgentOutput(text) {
29006
+ const result = {
29007
+ specDetected: false,
29008
+ implementationDetected: false,
29009
+ tasksDetected: false
29010
+ };
29011
+ if (!this.builder.getSession().spec) {
29012
+ const spec = this.builder.tryParseSpecFromOutput(text);
29013
+ if (spec) {
29014
+ this.builder.setSpec(spec);
29015
+ await this.persistSpec(spec);
29016
+ result.specDetected = true;
29017
+ }
29018
+ }
29019
+ if (this.builder.getPhase() === "implementation") {
29020
+ if (this.trySaveImplementationPlan(text)) result.implementationDetected = true;
29021
+ }
29022
+ const session = this.builder.getSession();
29023
+ if (session.spec) {
29024
+ const built = await this.tryBuildTasksFromOutput(text);
29025
+ if (built) {
29026
+ result.tasksDetected = true;
29027
+ result.graphId = built;
29028
+ }
29029
+ }
29030
+ return result;
29031
+ }
29032
+ /**
29033
+ * Advance to the next phase (mirrors `/sdd approve`). When moving into the
29034
+ * executing phase, guarantees a task graph exists — deterministically
29035
+ * generating one from the approved spec if the agent never emitted a valid
29036
+ * task array. Returns the new phase and its AI prompt.
29037
+ */
29038
+ async approve() {
29039
+ const phase = this.builder.approve();
29040
+ if (phase === "executing") {
29041
+ await this.ensureTaskGraph();
29042
+ }
29043
+ return { phase, prompt: this.builder.getAIPrompt() };
29044
+ }
29045
+ /**
29046
+ * Ensure a TaskGraph exists for the approved spec. If the agent already
29047
+ * produced one (via `ingestAgentOutput`), returns it; otherwise builds a
29048
+ * deterministic graph from the spec's requirements via TaskGenerator. This is
29049
+ * the robustness backstop: a run can always start, even if the model never
29050
+ * emitted a parseable task array.
29051
+ */
29052
+ async ensureTaskGraph() {
29053
+ if (this.graph) return this.graph;
29054
+ const spec = this.builder.getSession().spec;
29055
+ if (!spec) return null;
29056
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
29057
+ const generator = new TaskGenerator({
29058
+ taskTracker: tracker,
29059
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
29060
+ });
29061
+ const graph = await generator.generateFromSpec(spec);
29062
+ this.tracker = tracker;
29063
+ this.graph = graph;
29064
+ await this.persistGraph(graph);
29065
+ this.builder.setTaskGraphId(graph.id);
29066
+ await this.builder.saveSession();
29067
+ return graph;
29068
+ }
29069
+ snapshot() {
29070
+ const s = this.builder.getSession();
29071
+ const spec = s.spec;
29072
+ return {
29073
+ sessionId: s.id,
29074
+ phase: s.phase,
29075
+ title: s.title,
29076
+ questionCount: s.questionCount,
29077
+ minQuestions: this.minQuestions,
29078
+ maxQuestions: this.maxQuestions,
29079
+ answers: s.answers.map((a) => ({ question: a.question, answer: a.answer })),
29080
+ spec: spec ? {
29081
+ id: spec.id,
29082
+ title: spec.title,
29083
+ overview: spec.overview,
29084
+ requirements: spec.requirements.map((r) => ({
29085
+ priority: r.priority,
29086
+ description: r.description
29087
+ }))
29088
+ } : void 0,
29089
+ graphId: s.taskGraphId,
29090
+ taskCount: this.graph ? this.graph.nodes.size : 0,
29091
+ board: this.graph ? buildBoardTasks(this.graph) : void 0,
29092
+ prompt: this.builder.getAIPrompt()
29093
+ };
29094
+ }
29095
+ // ── internals ────────────────────────────────────────────────────────────
29096
+ async persistSpec(spec) {
29097
+ try {
29098
+ await this.o.specStore.save(spec);
29099
+ } catch {
29100
+ }
29101
+ }
29102
+ async persistGraph(graph) {
29103
+ try {
29104
+ await this.o.graphStore.save(graph);
29105
+ } catch {
29106
+ }
29107
+ }
29108
+ /**
29109
+ * Port of the CLI `trySaveImplementationPlan` operating on this driver's
29110
+ * builder. Captures the prose plan that precedes the task JSON block.
29111
+ */
29112
+ trySaveImplementationPlan(text) {
29113
+ const current = this.builder.getSession().implementation ?? "";
29114
+ const jsonStart = text.match(/```json\s*\[/);
29115
+ if (jsonStart?.index && jsonStart.index > 0) {
29116
+ const plan = text.substring(0, jsonStart.index).trim();
29117
+ if (plan.length > 50 && plan !== current && !isExplanatoryText(plan)) {
29118
+ this.builder.setImplementation(plan);
29119
+ return true;
29120
+ }
29121
+ }
29122
+ if (text.length > 100 && !text.includes("```json") && text.trim() !== current && !isExplanatoryText(text)) {
29123
+ this.builder.setImplementation(text.trim());
29124
+ return true;
29125
+ }
29126
+ return false;
29127
+ }
29128
+ /**
29129
+ * Port of the CLI `trySaveTasksFromAIOutput`: parse a task JSON array from the
29130
+ * agent output, build (or extend) the tracker + graph, persist to disk, and
29131
+ * link the graphId to the session. Returns the graphId on success.
29132
+ */
29133
+ async tryBuildTasksFromOutput(text) {
29134
+ const json = this.builder.extractJSONArray(text);
29135
+ if (!json) return void 0;
29136
+ let tasks;
29137
+ try {
29138
+ tasks = JSON.parse(json);
29139
+ } catch {
29140
+ return void 0;
29141
+ }
29142
+ const valid = tasks.filter(
29143
+ (t2) => t2 && typeof t2 === "object" && typeof t2.title === "string" && t2.title.length > 0
29144
+ );
29145
+ if (valid.length === 0) return void 0;
29146
+ const spec = this.builder.getSession().spec;
29147
+ if (!spec) return void 0;
29148
+ if (!this.tracker || !this.graph) {
29149
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
29150
+ this.graph = await tracker.createGraph(spec.id, spec.title);
29151
+ this.tracker = tracker;
29152
+ }
29153
+ const refMap = /* @__PURE__ */ new Map();
29154
+ const created = [];
29155
+ valid.forEach((task, i) => {
29156
+ const node = addTaskToTracker(this.tracker, task);
29157
+ created.push({ nodeId: node.id, task });
29158
+ if (typeof task.id === "string" && task.id.trim()) {
29159
+ refMap.set(task.id.trim().toLowerCase(), node.id);
29160
+ }
29161
+ refMap.set(`t${i + 1}`, node.id);
29162
+ refMap.set(String(i + 1), node.id);
29163
+ refMap.set(normalizeTaskRef(String(task.title)), node.id);
29164
+ });
29165
+ for (const { nodeId, task } of created) {
29166
+ const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
29167
+ for (const ref of deps) {
29168
+ const depId = refMap.get(normalizeTaskRef(String(ref)));
29169
+ if (depId && depId !== nodeId) this.tracker.addDependency(depId, nodeId);
29170
+ }
29171
+ }
29172
+ await this.persistGraph(this.graph);
29173
+ this.builder.setTaskGraphId(this.graph.id);
29174
+ await this.builder.saveSession();
29175
+ return this.graph.id;
29176
+ }
29177
+ };
29178
+ var TASK_TYPES2 = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
29179
+ var TASK_PRIORITIES = ["critical", "high", "medium", "low"];
29180
+ function normalizeTaskRef(ref) {
29181
+ return ref.trim().toLowerCase();
29182
+ }
29183
+ function addTaskToTracker(tracker, task) {
29184
+ return tracker.addNode({
29185
+ title: String(task.title),
29186
+ description: String(task.description ?? ""),
29187
+ type: TASK_TYPES2.includes(String(task.type)) ? String(task.type) : "feature",
29188
+ priority: TASK_PRIORITIES.includes(String(task.priority)) ? String(task.priority) : "medium",
29189
+ status: "pending",
29190
+ estimateHours: Number(task.estimateHours) || 2,
29191
+ tags: Array.isArray(task.tags) ? task.tags.map(String) : []
29192
+ });
29193
+ }
29194
+ function isExplanatoryText(text) {
29195
+ const lower = text.toLowerCase();
29196
+ return lower.startsWith("i'") || lower.startsWith("i will") || lower.startsWith("let me") || lower.startsWith("here's my") || lower.startsWith("here is my") || lower.startsWith("i'm going to") || lower.startsWith("first, let me") || lower.startsWith("sure") || lower.startsWith("of course") || lower.startsWith("okay") || lower.startsWith("ok,") || lower.startsWith("sounds good") || lower.startsWith("no problem") || text.split("\n").length < 3 && !text.includes(".");
29197
+ }
29198
+
29199
+ // src/sdd/start-sdd-run.ts
29200
+ function startSddRun(opts) {
29201
+ SddParallelRun.resetOrphans(opts.tracker);
29202
+ const run = new SddParallelRun({
29203
+ tracker: opts.tracker,
29204
+ graph: opts.graph,
29205
+ agent: opts.agent,
29206
+ projectRoot: opts.projectRoot,
29207
+ parallelSlots: opts.parallelSlots,
29208
+ taskTimeoutMs: opts.taskTimeoutMs,
29209
+ taskIdleTimeoutMs: opts.taskIdleTimeoutMs,
29210
+ maxFailedRetrySweeps: opts.maxFailedRetrySweeps,
29211
+ verifyTask: opts.verifyTask,
29212
+ conflictResolver: opts.conflictResolver,
29213
+ superviseFailure: opts.superviseFailure,
29214
+ subagentFactory: opts.subagentFactory,
29215
+ events: opts.events,
29216
+ worktrees: opts.worktrees,
29217
+ maxRecoveryRounds: opts.maxRecoveryRounds ?? 1,
29218
+ onProgress: opts.onProgress,
29219
+ defaultModel: opts.defaultModel,
29220
+ defaultProvider: opts.defaultProvider,
29221
+ fallbackModels: opts.fallbackModels
29222
+ });
29223
+ const projector = new SddBoardProjector({
29224
+ runId: run.runId,
29225
+ graph: opts.graph,
29226
+ tracker: opts.tracker,
29227
+ events: opts.events,
29228
+ store: opts.boardStore,
29229
+ specId: opts.graph.specId,
29230
+ defaultModel: opts.defaultModel,
29231
+ defaultProvider: opts.defaultProvider,
29232
+ fallbackModels: opts.fallbackModels
29233
+ });
29234
+ opts.registry?.register({
29235
+ runId: run.runId,
29236
+ specId: opts.graph.specId,
29237
+ pause: () => run.pause(),
29238
+ resume: () => run.resume(),
29239
+ stop: () => run.stop(),
29240
+ retryTask: (id) => run.retryTask(id),
29241
+ retryAllFailed: () => run.retryAllFailed(),
29242
+ reassignTask: (id, name) => run.reassignTask(id, name),
29243
+ setTaskModel: (id, model, provider) => run.setTaskModel(id, model, provider),
29244
+ setTaskFallbacks: (id, fb) => run.setTaskFallbacks(id, fb),
29245
+ setTaskVerification: (id, cmd) => run.setTaskVerification(id, cmd),
29246
+ cancelTask: (id) => run.cancelTask(id),
29247
+ deleteTask: (id) => run.deleteTask(id),
29248
+ splitTask: (id, subtasks) => run.splitTask(id, subtasks),
29249
+ cleanupWorktrees: () => run.cleanupWorktrees(),
29250
+ rollback: () => run.rollback(),
29251
+ getBaseBranch: () => run.getBaseBranch(),
29252
+ getMergedCommits: () => run.getMergedCommits(),
29253
+ snapshot: () => projector.snapshot(),
29254
+ isRunning: () => run.isRunning()
29255
+ });
29256
+ const drainMs = opts.controlDrainMs ?? 500;
29257
+ const controlTimer = setInterval(() => {
29258
+ void opts.boardStore.drainControl(run.runId).then((cmds) => {
29259
+ for (const c of cmds) {
29260
+ const p = c.payload ?? {};
29261
+ if (c.type === "pause") run.pause();
29262
+ else if (c.type === "resume") run.resume();
29263
+ else if (c.type === "stop") run.stop();
29264
+ else if (c.type === "retry" && p.taskId) run.retryTask(p.taskId);
29265
+ else if (c.type === "retry_all_failed") run.retryAllFailed();
29266
+ else if (c.type === "reassign" && p.taskId) run.reassignTask(p.taskId, p.agentName ?? "");
29267
+ else if (c.type === "set_task_model" && p.taskId) run.setTaskModel(p.taskId, p.model, p.provider);
29268
+ else if (c.type === "set_task_fallbacks" && p.taskId) run.setTaskFallbacks(p.taskId, p.fallbackModels);
29269
+ else if (c.type === "set_task_verification" && p.taskId)
29270
+ run.setTaskVerification(p.taskId, p.verificationCommand);
29271
+ else if (c.type === "cancel_task" && p.taskId) void run.cancelTask(p.taskId);
29272
+ else if (c.type === "delete_task" && p.taskId) run.deleteTask(p.taskId);
29273
+ else if (c.type === "split_task" && p.taskId && p.subtasks?.length) run.splitTask(p.taskId, p.subtasks);
29274
+ else if (c.type === "cleanup_worktrees") void run.cleanupWorktrees();
29275
+ else if (c.type === "rollback") void run.rollback();
29276
+ }
29277
+ });
29278
+ }, drainMs);
29279
+ controlTimer.unref?.();
29280
+ const completion = (async () => {
29281
+ try {
29282
+ return await run.run();
29283
+ } finally {
29284
+ clearInterval(controlTimer);
29285
+ await projector.drain().catch(() => {
29286
+ });
29287
+ projector.dispose();
29288
+ opts.registry?.clear(run.runId);
29289
+ }
29290
+ })();
29291
+ return {
29292
+ run,
29293
+ runId: run.runId,
29294
+ projector,
29295
+ completion,
29296
+ stop: () => run.stop()
29297
+ };
29298
+ }
29299
+ var MAX_SLUG = 40;
29300
+ var WorktreeManager = class {
29301
+ projectRoot;
29302
+ events;
29303
+ gitBin;
29304
+ runGit;
29305
+ /** Keyed by ownerId. */
29306
+ handles = /* @__PURE__ */ new Map();
29307
+ usedSlugs = /* @__PURE__ */ new Set();
29308
+ constructor(opts) {
29309
+ this.projectRoot = resolve(opts.projectRoot);
29310
+ this.events = opts.events;
29311
+ this.gitBin = opts.gitBin ?? "git";
29312
+ this.runGit = opts.run ?? ((args, cwd) => this.defaultRun(args, cwd));
29313
+ }
29314
+ /** Create a fresh worktree + branch forked from the current base branch. */
29315
+ async allocate(ownerId, opts = {}) {
29316
+ const existing = this.handles.get(ownerId);
29317
+ if (existing && (existing.status === "allocating" || existing.status === "active")) {
29318
+ return existing;
29319
+ }
29320
+ const slug = this.makeSlug(opts.slugHint ?? ownerId);
29321
+ const branch = `wstack/ap/${slug}`;
29322
+ const dir = join(this.worktreesRoot(), slug);
29323
+ const absDir = resolve(dir);
29324
+ const absRoot = resolve(this.projectRoot);
29325
+ if (!absDir.startsWith(absRoot + sep)) {
29326
+ throw new Error(`Worktree dir "${absDir}" resolves outside project root`);
29327
+ }
29328
+ const baseBranch = opts.baseBranch ?? await this.detectBaseBranch();
29329
+ const handle = {
29330
+ id: slug,
29331
+ ownerId,
29332
+ ownerLabel: opts.ownerLabel ?? opts.slugHint ?? ownerId,
29333
+ slug,
29334
+ dir,
29335
+ branch,
29336
+ baseBranch,
29337
+ status: "allocating",
29338
+ createdAt: Date.now(),
29339
+ updatedAt: Date.now(),
29340
+ insertions: 0,
29341
+ deletions: 0,
29342
+ files: 0
29343
+ };
29344
+ this.handles.set(ownerId, handle);
29345
+ try {
29346
+ await mkdir(this.worktreesRoot(), { recursive: true });
29347
+ const res = await this.runGit(
29348
+ ["worktree", "add", "-b", branch, dir, baseBranch],
29349
+ this.projectRoot
29350
+ );
29351
+ if (res.code !== 0) {
29352
+ return this.fail(handle, res.stderr || "git worktree add failed");
29353
+ }
29354
+ } catch (err) {
29355
+ return this.fail(handle, toErrorMessage(err));
29356
+ }
29357
+ this.setStatus(handle, "active");
29358
+ this.emit("worktree.allocated", {
29359
+ handleId: handle.id,
29360
+ ownerId: handle.ownerId,
29361
+ ownerLabel: handle.ownerLabel,
29362
+ slug: handle.slug,
29363
+ dir: handle.dir,
29364
+ branch: handle.branch,
29365
+ baseBranch: handle.baseBranch
29366
+ });
29367
+ return handle;
29368
+ }
29369
+ /** Stage everything and commit inside the worktree. */
29370
+ async commitAll(handle, message) {
29371
+ this.setStatus(handle, "committing");
29372
+ await this.runGit(["add", "-A"], handle.dir);
29373
+ const staged = await this.runGit(["diff", "--cached", "--quiet"], handle.dir);
29374
+ if (staged.code === 0) {
29375
+ this.emitCommitted(handle, false);
29376
+ return { committed: false };
29377
+ }
29378
+ const idArgs = await this.identityArgs(handle.dir);
29379
+ const committed = await this.runGit([...idArgs, "commit", "-m", message], handle.dir);
29380
+ if (committed.code !== 0) {
29381
+ this.fail(handle, committed.stderr || "git commit failed");
29382
+ return { committed: false };
29383
+ }
29384
+ const stats = await this.collectStats(handle.dir);
29385
+ handle.insertions = stats.insertions;
29386
+ handle.deletions = stats.deletions;
29387
+ handle.files = stats.files;
29388
+ handle.sha = stats.sha;
29389
+ handle.updatedAt = Date.now();
29390
+ this.emitCommitted(handle, true);
29391
+ return { committed: true };
29392
+ }
29393
+ /** Merge the worktree branch back into the base branch (squash by default). */
29394
+ async merge(handle, opts = {}) {
29395
+ const squash = opts.squash ?? true;
29396
+ this.setStatus(handle, "merging");
29397
+ const checkout = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
29398
+ if (checkout.code !== 0) {
29399
+ this.fail(handle, checkout.stderr || `checkout ${handle.baseBranch} failed`);
29400
+ return { ok: false, stderr: checkout.stderr };
29401
+ }
29402
+ const mergeArgs = squash ? ["merge", "--squash", handle.branch] : ["merge", "--no-ff", handle.branch];
29403
+ const merged = await this.runGit(mergeArgs, this.projectRoot);
29404
+ if (merged.code !== 0) {
29405
+ const fromOutput = parseConflictPaths(`${merged.stdout}
29406
+ ${merged.stderr}`);
29407
+ const fromIndex = await this.unmergedFiles();
29408
+ const conflictFiles = [.../* @__PURE__ */ new Set([...fromOutput, ...fromIndex])];
29409
+ if (opts.resolve) {
29410
+ const finalized = await this.tryResolveConflict(handle, conflictFiles, opts);
29411
+ if (finalized) return finalized;
29412
+ }
29413
+ await this.runGit(["reset", "--hard", "HEAD"], this.projectRoot);
29414
+ handle.conflictFiles = conflictFiles;
29415
+ this.setStatus(handle, "needs-review", { lastError: merged.stderr });
29416
+ this.emit("worktree.conflict", {
29417
+ handleId: handle.id,
29418
+ ownerId: handle.ownerId,
29419
+ branch: handle.branch,
29420
+ conflictFiles
29421
+ });
29422
+ return { ok: false, conflict: true, conflictFiles, stderr: merged.stderr };
29423
+ }
29424
+ if (squash) {
29425
+ const msg = opts.message ?? `merge ${handle.branch} (squash)`;
29426
+ const idArgs = await this.identityArgs(this.projectRoot);
29427
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
29428
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
29429
+ this.fail(handle, commit.stderr || "squash commit failed");
29430
+ return { ok: false, stderr: commit.stderr };
29431
+ }
29432
+ }
29433
+ this.setStatus(handle, "merged");
29434
+ this.emit("worktree.merged", {
29435
+ handleId: handle.id,
29436
+ ownerId: handle.ownerId,
29437
+ branch: handle.branch,
29438
+ baseBranch: handle.baseBranch,
29439
+ squash
29440
+ });
29441
+ return { ok: true };
29442
+ }
29443
+ /**
29444
+ * Current tip SHA of a handle's base branch (without checking it out). Capture
29445
+ * this before a merge so a regressed merge can be reverted to exactly this
29446
+ * commit — unambiguous even when a squash produced no diff. Returns null on
29447
+ * failure (caller then skips the revert).
29448
+ */
29449
+ async baseHead(handle) {
29450
+ const res = await this.runGit(["rev-parse", handle.baseBranch], this.projectRoot);
29451
+ const sha = res.stdout.trim();
29452
+ return res.code === 0 && sha ? sha : null;
29453
+ }
29454
+ /**
29455
+ * Hard-reset the base branch back to `sha` (a value previously returned by
29456
+ * {@link baseHead}). Used to undo a squash-merge whose integrated result failed
29457
+ * re-verification, so an auto-resolved-but-broken merge never sticks on base.
29458
+ * Safe because SDD merges are serialized — no other commit lands in between.
29459
+ */
29460
+ async revertBaseTo(handle, sha) {
29461
+ const co = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
29462
+ if (co.code !== 0) return false;
29463
+ const reset = await this.runGit(["reset", "--hard", sha], this.projectRoot);
29464
+ return reset.code === 0;
29465
+ }
29466
+ /**
29467
+ * Current base branch + tip SHA, captured WITHOUT a handle. The SDD run calls
29468
+ * this once at start so a later rollback knows which branch the run's squash
29469
+ * commits landed on. Returns null when not in a usable git state.
29470
+ */
29471
+ async currentBase() {
29472
+ const branch = await this.detectBaseBranch();
29473
+ const head = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
29474
+ const sha = head.stdout.trim();
29475
+ return head.code === 0 && sha ? { branch, sha } : null;
29476
+ }
29477
+ /**
29478
+ * Force-remove EVERY managed worktree + branch this project owns, without
29479
+ * relying on the in-memory `handles` map — so it works post-run (a fresh
29480
+ * manager can clean up a previous run's leftovers). Enumerates
29481
+ * `git worktree list --porcelain`, removes every checkout living under the
29482
+ * `.wrongstack/worktrees` root, deletes every `wstack/ap/*` branch, then prunes.
29483
+ * Returns the number of worktrees removed. Never throws — best-effort cleanup.
29484
+ */
29485
+ async cleanupAllManaged() {
29486
+ const root = resolve(this.worktreesRoot());
29487
+ let removed = 0;
29488
+ try {
29489
+ const listed = await this.runGit(["worktree", "list", "--porcelain"], this.projectRoot);
29490
+ for (const line of listed.stdout.split("\n")) {
29491
+ const m = line.match(/^worktree\s+(.+?)\s*$/);
29492
+ if (!m?.[1]) continue;
29493
+ const dir = resolve(m[1]);
29494
+ if (dir !== root && (dir === root || dir.startsWith(root + sep))) {
29495
+ const rm6 = await this.runGit(["worktree", "remove", "--force", dir], this.projectRoot);
29496
+ if (rm6.code === 0) removed++;
29497
+ }
29498
+ }
29499
+ } catch {
29500
+ }
29501
+ try {
29502
+ const branches = await this.runGit(
29503
+ ["branch", "--list", "--format=%(refname:short)", "wstack/ap/*"],
29504
+ this.projectRoot
29505
+ );
29506
+ for (const b of branches.stdout.split("\n").map((s) => s.trim()).filter(Boolean)) {
29507
+ await this.runGit(["branch", "-D", b], this.projectRoot);
29508
+ }
29509
+ } catch {
29510
+ }
29511
+ await this.runGit(["worktree", "prune"], this.projectRoot).catch(() => void 0);
29512
+ this.handles.clear();
29513
+ this.emit("worktree.released", {
29514
+ handleId: "cleanup-all",
29515
+ ownerId: "cleanup-all",
29516
+ branch: "wstack/ap/*",
29517
+ kept: false
29518
+ });
29519
+ return { removed };
29520
+ }
29521
+ /**
29522
+ * Undo a run's squash commits by reverting each (newest → oldest) on the base
29523
+ * branch — history-preserving, never a destructive reset. Refuses on a dirty
29524
+ * working tree (so uncommitted work is never clobbered) and aborts cleanly if a
29525
+ * revert conflicts, reporting which SHA. `shas` are the run commit SHAs in the
29526
+ * order they landed; this reverses them. Returns the count reverted.
29527
+ */
29528
+ async revertCommits(baseBranch, shas) {
29529
+ if (shas.length === 0) return { ok: true, reverted: 0, reason: "nothing to revert" };
29530
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
29531
+ if (status.stdout.trim().length > 0) {
29532
+ return { ok: false, reverted: 0, reason: "working tree has uncommitted changes \u2014 commit or stash first" };
29533
+ }
29534
+ const co = await this.runGit(["checkout", baseBranch], this.projectRoot);
29535
+ if (co.code !== 0) {
29536
+ return { ok: false, reverted: 0, reason: co.stderr || `checkout ${baseBranch} failed` };
29537
+ }
29538
+ const idArgs = await this.identityArgs(this.projectRoot);
29539
+ let reverted = 0;
29540
+ for (const sha of [...shas].reverse()) {
29541
+ const res = await this.runGit([...idArgs, "revert", "--no-edit", sha], this.projectRoot);
29542
+ if (res.code !== 0) {
29543
+ await this.runGit(["revert", "--abort"], this.projectRoot).catch(() => void 0);
29544
+ return {
29545
+ ok: false,
29546
+ reverted,
29547
+ reason: `revert of ${sha.slice(0, 8)} failed: ${(res.stderr || res.stdout).trim().split("\n")[0] ?? "conflict"}`
29548
+ };
29549
+ }
29550
+ reverted++;
29551
+ }
29552
+ return { ok: true, reverted };
29553
+ }
29554
+ /**
29555
+ * Run the caller-supplied resolver against a conflicted squash-merge, then
29556
+ * commit if it cleared every marker. Returns a successful `MergeResult` on a
29557
+ * clean resolution, or `null` to signal the caller should fall back to the
29558
+ * abort path. Never leaves the base tree committed-but-dirty: a partial or
29559
+ * failed resolution returns `null` and the caller hard-resets.
29560
+ */
29561
+ async tryResolveConflict(handle, conflictFiles, opts) {
29562
+ let resolved = false;
29563
+ try {
29564
+ resolved = opts.resolve ? await opts.resolve({ conflictFiles, cwd: this.projectRoot }) : false;
29565
+ } catch {
29566
+ resolved = false;
29567
+ }
29568
+ if (!resolved) return null;
29569
+ await this.runGit(["add", "-A"], this.projectRoot);
29570
+ if (await this.hasConflictMarkers()) return null;
29571
+ const idArgs = await this.identityArgs(this.projectRoot);
29572
+ const msg = opts.message ?? `merge ${handle.branch} (squash, conflict resolved)`;
29573
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
29574
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
29575
+ return null;
29576
+ }
29577
+ handle.conflictFiles = conflictFiles;
29578
+ this.setStatus(handle, "merged");
29579
+ this.emit("worktree.merged", {
29580
+ handleId: handle.id,
29581
+ ownerId: handle.ownerId,
29582
+ branch: handle.branch,
29583
+ baseBranch: handle.baseBranch,
29584
+ squash: true
29585
+ });
29586
+ return { ok: true, resolved: true, conflictFiles };
29587
+ }
29588
+ /**
29589
+ * True when staged content still carries conflict markers. `git diff --cached
29590
+ * --check` exits nonzero and prints a "leftover conflict marker" line for each
29591
+ * survivor; whitespace-only errors (also flagged by --check) are ignored so a
29592
+ * clean resolution with unrelated whitespace is not rejected.
29593
+ */
29594
+ async hasConflictMarkers() {
29595
+ const check = await this.runGit(["diff", "--cached", "--check"], this.projectRoot);
29596
+ if (check.code === 0) return false;
29597
+ return /conflict marker/i.test(`${check.stdout}
29598
+ ${check.stderr}`);
29599
+ }
29600
+ /**
29601
+ * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`)
29602
+ * are left on disk for inspection.
29603
+ */
29604
+ async release(handle, opts = {}) {
29605
+ const keep = opts.keep || handle.status === "needs-review" || handle.status === "failed";
29606
+ if (!keep) {
29607
+ await this.runGit(["worktree", "remove", "--force", handle.dir], this.projectRoot);
29608
+ await this.runGit(["branch", "-D", handle.branch], this.projectRoot);
29609
+ await this.runGit(["worktree", "prune"], this.projectRoot);
29610
+ this.handles.delete(handle.ownerId);
29611
+ }
29612
+ this.emit("worktree.released", {
29613
+ handleId: handle.id,
29614
+ ownerId: handle.ownerId,
29615
+ branch: handle.branch,
29616
+ kept: keep
29617
+ });
29618
+ }
29619
+ get(ownerId) {
29620
+ return this.handles.get(ownerId);
29621
+ }
29622
+ list() {
29623
+ return [...this.handles.values()];
29624
+ }
29625
+ // ── internals ────────────────────────────────────────────────────────────
29626
+ worktreesRoot() {
29627
+ return join(this.projectRoot, ".wrongstack", "worktrees");
29628
+ }
29629
+ async detectBaseBranch() {
29630
+ const head = await this.runGit(["rev-parse", "--abbrev-ref", "HEAD"], this.projectRoot);
29631
+ const name = head.stdout.trim();
29632
+ if (name && name !== "HEAD") return name;
29633
+ const sha = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
29634
+ return sha.stdout.trim() || "HEAD";
29635
+ }
29636
+ makeSlug(hint) {
29637
+ let base = hint.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+/, "").replace(/[-.]+$/, "").slice(0, MAX_SLUG).replace(/[-.]+$/, "");
29638
+ if (!base) base = "wt";
29639
+ let slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
29640
+ while (this.usedSlugs.has(slug)) slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
29641
+ this.usedSlugs.add(slug);
29642
+ return slug;
29643
+ }
29644
+ async collectStats(dir) {
29645
+ const sha = (await this.runGit(["rev-parse", "HEAD"], dir)).stdout.trim();
29646
+ const numstat = await this.runGit(["show", "--numstat", "--format=", "HEAD"], dir);
29647
+ let insertions = 0;
29648
+ let deletions = 0;
29649
+ let files = 0;
29650
+ for (const line of numstat.stdout.split("\n")) {
29651
+ const m = line.trim().match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
29652
+ if (!m) continue;
29653
+ files++;
29654
+ if (m[1] !== "-") insertions += Number(m[1]);
29655
+ if (m[2] !== "-") deletions += Number(m[2]);
29656
+ }
29657
+ return { insertions, deletions, files, sha };
29658
+ }
29659
+ /**
29660
+ * `git -c user.*` fallback so commits succeed on machines and CI runners
29661
+ * that have no global git identity configured. Returns `[]` when both
29662
+ * `user.name` and `user.email` are already set (the common case), so a real
29663
+ * user's identity is never overridden. The worktree branch commits are
29664
+ * squashed away on merge, so the fallback identity never reaches the base
29665
+ * branch history.
29666
+ */
29667
+ async identityArgs(cwd) {
29668
+ const name = (await this.runGit(["config", "user.name"], cwd)).stdout.trim();
29669
+ const email = (await this.runGit(["config", "user.email"], cwd)).stdout.trim();
29670
+ if (name && email) return [];
29671
+ return [
29672
+ "-c",
29673
+ `user.name=${name || "AutoPhase"}`,
29674
+ "-c",
29675
+ `user.email=${email || "autophase@agent.local"}`
29676
+ ];
29677
+ }
29678
+ async unmergedFiles() {
29679
+ const res = await this.runGit(["diff", "--name-only", "--diff-filter=U"], this.projectRoot);
29680
+ return res.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
29681
+ }
29682
+ emitCommitted(handle, committed) {
29683
+ this.emit("worktree.committed", {
29684
+ handleId: handle.id,
29685
+ ownerId: handle.ownerId,
29686
+ branch: handle.branch,
29687
+ committed,
29688
+ insertions: handle.insertions,
29689
+ deletions: handle.deletions,
29690
+ files: handle.files,
29691
+ sha: handle.sha
29692
+ });
29693
+ }
29694
+ fail(handle, error) {
29695
+ this.setStatus(handle, "failed", { lastError: error });
29696
+ this.emit("worktree.failed", {
29697
+ handleId: handle.id,
29698
+ ownerId: handle.ownerId,
29699
+ branch: handle.branch,
29700
+ error
29701
+ });
29702
+ return handle;
29703
+ }
29704
+ setStatus(handle, status, patch) {
29705
+ handle.status = status;
29706
+ handle.updatedAt = Date.now();
29707
+ if (patch) Object.assign(handle, patch);
29708
+ }
29709
+ emit(event, payload) {
29710
+ this.events?.emit(event, payload);
29711
+ }
29712
+ defaultRun(args, cwd) {
29713
+ return new Promise((res) => {
29714
+ let stdout = "";
29715
+ let stderr = "";
29716
+ const MAX_GIT_OUTPUT = 1e6;
29717
+ const child = spawn(this.gitBin, args, {
29718
+ cwd,
29719
+ env: buildChildEnv(),
29720
+ stdio: ["ignore", "pipe", "pipe"],
29721
+ signal: AbortSignal.timeout(3e4),
29722
+ windowsHide: true
29723
+ });
29724
+ child.stdout?.on("data", (c) => {
29725
+ if (stdout.length < MAX_GIT_OUTPUT) stdout += c.toString();
29726
+ });
29727
+ child.stderr?.on("data", (c) => {
29728
+ if (stderr.length < MAX_GIT_OUTPUT) stderr += c.toString();
29729
+ });
29730
+ child.on("error", (err) => res({ code: 1, stdout, stderr: err.message }));
29731
+ child.on("close", (code) => res({ code: code ?? 1, stdout, stderr }));
29732
+ });
29733
+ }
29734
+ };
29735
+ function parseConflictPaths(output) {
29736
+ const paths = /* @__PURE__ */ new Set();
29737
+ for (const line of output.split("\n")) {
29738
+ const m = line.match(/^CONFLICT \([^)]*\): Merge conflict in (.+?)\s*$/);
29739
+ if (m?.[1]) paths.add(m[1]);
29740
+ }
29741
+ return [...paths];
29742
+ }
29743
+ function assertSafePath(dir, projectRoot) {
29744
+ const root = resolve(projectRoot);
29745
+ const abs = resolve(dir);
29746
+ if (abs !== root && !abs.startsWith(root + sep)) {
29747
+ throw new Error(`worktree path escapes project root: ${dir}`);
29748
+ }
29749
+ }
29750
+
29751
+ // src/sdd/sdd-lifecycle.ts
29752
+ async function cleanupSddWorktrees(projectRoot) {
29753
+ const wt = new WorktreeManager({ projectRoot });
29754
+ return wt.cleanupAllManaged();
29755
+ }
29756
+ async function rollbackSddRunFromDisk(opts) {
29757
+ const store = new SddBoardStore({ baseDir: opts.boardsDir });
29758
+ const runId = opts.runId ?? (await store.list())[0]?.runId;
29759
+ if (!runId) return { ok: false, reverted: 0, reason: "no SDD board found to roll back" };
29760
+ const snap = await store.load(runId);
29761
+ if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
29762
+ if (!snap.baseBranch) {
29763
+ return { ok: false, reverted: 0, reason: "this run did not record a base branch (no worktree run)" };
29764
+ }
29765
+ const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
29766
+ if (shas.length === 0) {
29767
+ return { ok: false, reverted: 0, reason: "no merged commits recorded for this run" };
29768
+ }
29769
+ const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
29770
+ return wt.revertCommits(snap.baseBranch, shas);
29771
+ }
29772
+ async function destroySddProject(opts) {
29773
+ const { removed } = await cleanupSddWorktrees(opts.projectRoot).catch(() => ({ removed: 0 }));
29774
+ const deleted = [];
29775
+ const rmDir = async (dir, label) => {
29776
+ try {
29777
+ await fsp3.rm(dir, { recursive: true, force: true });
29778
+ deleted.push(label);
29779
+ } catch {
29780
+ }
29781
+ };
29782
+ const rmFile = async (file, label) => {
29783
+ try {
29784
+ await fsp3.unlink(file);
29785
+ deleted.push(label);
29786
+ } catch {
29787
+ }
29788
+ };
29789
+ await rmFile(opts.paths.projectSddSession, "session");
29790
+ await rmDir(opts.paths.projectSpecs, "specs");
29791
+ await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
29792
+ await rmDir(opts.paths.projectSddBoards, "boards");
29793
+ return { worktreesRemoved: removed, deleted };
29794
+ }
29795
+
29796
+ // src/observability/metrics.ts
29797
+ var RESERVOIR_SIZE = 1024;
29798
+ function labelKey(labels) {
29799
+ if (!labels) return "";
29800
+ const keys = Object.keys(labels).sort();
29801
+ return keys.map((k) => `${k}=${labels[k]}`).join(",");
29802
+ }
29803
+ function quantile(sorted, q) {
29804
+ if (sorted.length === 0) return 0;
29805
+ const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length));
29806
+ return sorted[idx] ?? 0;
29807
+ }
29808
+ var InMemoryMetricsSink = class {
29809
+ counters = /* @__PURE__ */ new Map();
29810
+ gauges = /* @__PURE__ */ new Map();
29811
+ histograms = /* @__PURE__ */ new Map();
29812
+ counter(name, value = 1, labels) {
29813
+ const series = this.getOrCreate(this.counters, name);
29814
+ const key = labelKey(labels);
29815
+ const state = series.get(key) ?? { value: 0 };
29816
+ state.value += value;
29817
+ series.set(key, state);
29818
+ }
29819
+ gauge(name, value, labels) {
29820
+ const series = this.getOrCreate(this.gauges, name);
29821
+ series.set(labelKey(labels), { value });
29822
+ }
29823
+ histogram(name, value, labels) {
29824
+ const series = this.getOrCreate(this.histograms, name);
29825
+ const key = labelKey(labels);
29826
+ let state = series.get(key);
29827
+ if (!state) {
29828
+ state = { count: 0, sum: 0, min: value, max: value, samples: [] };
29829
+ series.set(key, state);
29830
+ }
29831
+ state.count++;
29832
+ state.sum += value;
29833
+ if (value < state.min) state.min = value;
29834
+ if (value > state.max) state.max = value;
29835
+ if (state.samples.length < RESERVOIR_SIZE) {
29836
+ state.samples.push(value);
29837
+ } else {
29838
+ const r = Math.floor(Math.random() * state.count);
29839
+ if (r < RESERVOIR_SIZE) state.samples[r] = value;
29840
+ }
29841
+ }
29842
+ snapshot() {
29843
+ const series = [];
29844
+ for (const [name, byLabel] of this.counters) {
29845
+ for (const [key, state] of byLabel) {
29846
+ series.push({
29847
+ name,
29848
+ type: "counter",
29849
+ labels: parseLabelKey(key),
29850
+ values: { value: state.value }
29851
+ });
29852
+ }
29853
+ }
29854
+ for (const [name, byLabel] of this.gauges) {
29855
+ for (const [key, state] of byLabel) {
29856
+ series.push({
29857
+ name,
29858
+ type: "gauge",
29859
+ labels: parseLabelKey(key),
29860
+ values: { value: state.value }
29861
+ });
29862
+ }
29863
+ }
29864
+ for (const [name, byLabel] of this.histograms) {
29865
+ for (const [key, state] of byLabel) {
29866
+ const sorted = [...state.samples].sort((a, b) => a - b);
29867
+ series.push({
29868
+ name,
29869
+ type: "histogram",
29870
+ labels: parseLabelKey(key),
29871
+ values: {
29872
+ count: state.count,
29873
+ sum: state.sum,
29874
+ min: state.min,
29875
+ max: state.max,
29876
+ p50: quantile(sorted, 0.5),
29877
+ p95: quantile(sorted, 0.95),
29878
+ p99: quantile(sorted, 0.99)
29879
+ }
29880
+ });
29881
+ }
29882
+ }
29883
+ return { timestamp: Date.now(), series };
29884
+ }
29885
+ reset() {
29886
+ this.counters.clear();
29887
+ this.gauges.clear();
29888
+ this.histograms.clear();
29889
+ }
29890
+ getOrCreate(bag, name) {
29891
+ let series = bag.get(name);
29892
+ if (!series) {
29893
+ series = /* @__PURE__ */ new Map();
29894
+ bag.set(name, series);
29895
+ }
29896
+ return series;
29897
+ }
29898
+ };
29899
+ function parseLabelKey(key) {
29900
+ if (!key) return {};
29901
+ const labels = {};
29902
+ for (const pair of key.split(",")) {
29903
+ const eq = pair.indexOf("=");
29904
+ if (eq > 0) labels[pair.slice(0, eq)] = pair.slice(eq + 1);
29905
+ }
29906
+ return labels;
29907
+ }
29908
+ var NoopMetricsSink = class {
29909
+ counter() {
29910
+ }
29911
+ gauge() {
29912
+ }
29913
+ histogram() {
29914
+ }
29915
+ snapshot() {
29916
+ return { timestamp: Date.now(), series: [] };
29917
+ }
29918
+ reset() {
29919
+ }
29920
+ };
29921
+
29922
+ // src/observability/health.ts
29923
+ var SEVERITY = {
29924
+ healthy: 0,
29925
+ degraded: 1,
29926
+ unhealthy: 2
29927
+ };
29928
+ var DefaultHealthRegistry = class {
29929
+ checks = /* @__PURE__ */ new Map();
29930
+ timeoutMs;
29931
+ constructor(opts = {}) {
29932
+ this.timeoutMs = opts.timeoutMs ?? 5e3;
29933
+ }
29934
+ register(check) {
27204
29935
  this.checks.set(check.name, check);
27205
29936
  }
27206
29937
  unregister(name) {
@@ -27406,7 +30137,7 @@ async function startMetricsServer(opts) {
27406
30137
  const tls = opts.tls;
27407
30138
  const useHttps = !!(tls?.cert && tls?.key);
27408
30139
  const host = opts.host ?? "127.0.0.1";
27409
- const path51 = opts.path ?? "/metrics";
30140
+ const path52 = opts.path ?? "/metrics";
27410
30141
  const healthPath = opts.healthPath ?? "/healthz";
27411
30142
  const healthRegistry = opts.healthRegistry;
27412
30143
  const listener = (req, res) => {
@@ -27416,7 +30147,7 @@ async function startMetricsServer(opts) {
27416
30147
  return;
27417
30148
  }
27418
30149
  const url = req.url.split("?")[0];
27419
- if (url === path51) {
30150
+ if (url === path52) {
27420
30151
  let body;
27421
30152
  try {
27422
30153
  body = renderPrometheus(opts.sink.snapshot());
@@ -27480,7 +30211,7 @@ async function startMetricsServer(opts) {
27480
30211
  const protocol = useHttps ? "https" : "http";
27481
30212
  return {
27482
30213
  port: boundPort,
27483
- url: `${protocol}://${host}:${boundPort}${path51}`,
30214
+ url: `${protocol}://${host}:${boundPort}${path52}`,
27484
30215
  close: () => new Promise((resolve19, reject) => {
27485
30216
  server.close((err) => err ? reject(err) : resolve19());
27486
30217
  })
@@ -35306,16 +38037,16 @@ Use \`/security report <number>\` to view a specific report.` };
35306
38037
  }
35307
38038
  const index = Number.parseInt(reportId, 10) - 1;
35308
38039
  if (!Number.isNaN(index) && reports[index]) {
35309
- const { readFile: readFile49 } = await import('fs/promises');
35310
- const content = await readFile49(join(reportsDir, reports[index]), "utf-8");
38040
+ const { readFile: readFile51 } = await import('fs/promises');
38041
+ const content = await readFile51(join(reportsDir, reports[index]), "utf-8");
35311
38042
  return { message: `# Security Report
35312
38043
 
35313
38044
  ${content}` };
35314
38045
  }
35315
38046
  const match = reports.find((r) => r.includes(reportId));
35316
38047
  if (match) {
35317
- const { readFile: readFile49 } = await import('fs/promises');
35318
- const content = await readFile49(join(reportsDir, match), "utf-8");
38048
+ const { readFile: readFile51 } = await import('fs/promises');
38049
+ const content = await readFile51(join(reportsDir, match), "utf-8");
35319
38050
  return { message: `# Security Report
35320
38051
 
35321
38052
  ${content}` };
@@ -36413,6 +39144,7 @@ function makeMailboxTool(opts = {}) {
36413
39144
  category: "coordination",
36414
39145
  permission: "auto",
36415
39146
  mutating: true,
39147
+ capabilities: [ToolCapabilities.COORDINATION_MAIL],
36416
39148
  inputSchema: {
36417
39149
  type: "object",
36418
39150
  properties: {
@@ -36676,6 +39408,7 @@ function makeMailSendTool(opts = {}) {
36676
39408
  category: "coordination",
36677
39409
  permission: "auto",
36678
39410
  mutating: true,
39411
+ capabilities: [ToolCapabilities.COORDINATION_MAIL],
36679
39412
  inputSchema: {
36680
39413
  type: "object",
36681
39414
  properties: {
@@ -36735,6 +39468,7 @@ function makeMailInboxTool(opts = {}) {
36735
39468
  category: "coordination",
36736
39469
  permission: "auto",
36737
39470
  mutating: false,
39471
+ capabilities: [ToolCapabilities.COORDINATION_MAIL],
36738
39472
  inputSchema: {
36739
39473
  type: "object",
36740
39474
  properties: {
@@ -42620,8 +45354,8 @@ var InputBuilder = class {
42620
45354
  async registerFile(input) {
42621
45355
  const ref = await this.store.add({ ...input, kind: "file" });
42622
45356
  this.refs.push(ref);
42623
- const path51 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
42624
- return `[file:${path51}]`;
45357
+ const path52 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
45358
+ return `[file:${path52}]`;
42625
45359
  }
42626
45360
  /**
42627
45361
  * Whether `appendPaste(text)` would collapse the text to a placeholder
@@ -43614,6 +46348,7 @@ function compactTrigger(trigger) {
43614
46348
  // src/registry/tool-registry.ts
43615
46349
  var ToolRegistry = class _ToolRegistry {
43616
46350
  tools = /* @__PURE__ */ new Map();
46351
+ descriptionModes = /* @__PURE__ */ new Map();
43617
46352
  /** Monotonic version bumped on every registry mutation. */
43618
46353
  _version = 0;
43619
46354
  /** Cached `list()` result, frozen after build. Invalidated on _version change. */
@@ -43625,6 +46360,10 @@ var ToolRegistry = class _ToolRegistry {
43625
46360
  tool._estDefTokens = estimateToolDefTokens(tool);
43626
46361
  }
43627
46362
  }
46363
+ _prepareForStorage(tool) {
46364
+ const mode = this.descriptionModes.get(tool.name) ?? "extend";
46365
+ return applyToolDescriptionModeToTool(tool, mode);
46366
+ }
43628
46367
  register(tool, owner = "core") {
43629
46368
  if (this.tools.has(tool.name)) {
43630
46369
  throw new WrongStackError({
@@ -43642,8 +46381,9 @@ var ToolRegistry = class _ToolRegistry {
43642
46381
  context: { tool: tool.name }
43643
46382
  });
43644
46383
  }
43645
- this._stampDefTokens(tool);
43646
- this.tools.set(tool.name, { tool, owner });
46384
+ const stored = this._prepareForStorage(tool);
46385
+ this._stampDefTokens(stored);
46386
+ this.tools.set(tool.name, { tool: stored, owner });
43647
46387
  this._version++;
43648
46388
  }
43649
46389
  /**
@@ -43656,8 +46396,9 @@ var ToolRegistry = class _ToolRegistry {
43656
46396
  if (!tool.inputSchema || typeof tool.inputSchema !== "object") {
43657
46397
  return false;
43658
46398
  }
43659
- this._stampDefTokens(tool);
43660
- this.tools.set(tool.name, { tool, owner });
46399
+ const stored = this._prepareForStorage(tool);
46400
+ this._stampDefTokens(stored);
46401
+ this.tools.set(tool.name, { tool: stored, owner });
43661
46402
  this._version++;
43662
46403
  return true;
43663
46404
  }
@@ -43683,11 +46424,15 @@ var ToolRegistry = class _ToolRegistry {
43683
46424
  */
43684
46425
  registerDefault(tool, owner = "core") {
43685
46426
  if (this.tools.has(tool.name)) return;
43686
- this._stampDefTokens(tool);
43687
- this.tools.set(tool.name, { tool, owner });
46427
+ const stored = this._prepareForStorage(tool);
46428
+ this._stampDefTokens(stored);
46429
+ this.tools.set(tool.name, { tool: stored, owner });
46430
+ this._version++;
43688
46431
  }
43689
46432
  unregister(name) {
43690
- return this.tools.delete(name);
46433
+ const deleted = this.tools.delete(name);
46434
+ if (deleted) this._version++;
46435
+ return deleted;
43691
46436
  }
43692
46437
  /**
43693
46438
  * Override an existing tool. Throws if the tool is not already registered.
@@ -43702,8 +46447,10 @@ var ToolRegistry = class _ToolRegistry {
43702
46447
  context: { tool: name }
43703
46448
  });
43704
46449
  }
43705
- this._stampDefTokens(tool);
43706
- this.tools.set(name, { tool, owner });
46450
+ const stored = this._prepareForStorage(tool);
46451
+ this._stampDefTokens(stored);
46452
+ this.tools.set(name, { tool: stored, owner });
46453
+ this._version++;
43707
46454
  }
43708
46455
  /**
43709
46456
  * Wrap (decorate) an existing tool. The wrapper receives the current
@@ -43726,12 +46473,49 @@ var ToolRegistry = class _ToolRegistry {
43726
46473
  context: { tool: name }
43727
46474
  });
43728
46475
  }
43729
- const wrapped = wrapper(entry.tool);
46476
+ const current = applyToolDescriptionModeToTool(entry.tool, "extend");
46477
+ const wrapped = this._prepareForStorage(wrapper(current));
43730
46478
  wrapped._estDefTokens = void 0;
43731
46479
  this._stampDefTokens(wrapped);
43732
46480
  this.tools.set(name, { tool: wrapped, owner: `${entry.owner}+${owner}` });
43733
46481
  this._version++;
43734
46482
  }
46483
+ setDescriptionMode(name, mode) {
46484
+ const normalized = normalizeToolDescriptionMode(mode);
46485
+ if (!normalized) return false;
46486
+ const entry = this.tools.get(name);
46487
+ if (!entry) return false;
46488
+ if (normalized === "extend") {
46489
+ this.descriptionModes.delete(name);
46490
+ } else {
46491
+ this.descriptionModes.set(name, normalized);
46492
+ }
46493
+ const stored = applyToolDescriptionModeToTool(entry.tool, normalized);
46494
+ stored._estDefTokens = void 0;
46495
+ this._stampDefTokens(stored);
46496
+ this.tools.set(name, { ...entry, tool: stored });
46497
+ this._version++;
46498
+ return true;
46499
+ }
46500
+ getDescriptionMode(name) {
46501
+ return this.descriptionModes.get(name) ?? "extend";
46502
+ }
46503
+ applyDescriptionModes(modes = {}) {
46504
+ const missing = [];
46505
+ let applied = 0;
46506
+ for (const [name, rawMode] of Object.entries(modes)) {
46507
+ const mode = normalizeToolDescriptionMode(rawMode);
46508
+ if (!mode) continue;
46509
+ if (this.tools.has(name)) {
46510
+ if (this.setDescriptionMode(name, mode)) applied++;
46511
+ } else {
46512
+ if (mode === "simple") this.descriptionModes.set(name, mode);
46513
+ else this.descriptionModes.delete(name);
46514
+ missing.push(name);
46515
+ }
46516
+ }
46517
+ return { applied, missing };
46518
+ }
43735
46519
  get(name) {
43736
46520
  return this.tools.get(name)?.tool;
43737
46521
  }
@@ -43770,6 +46554,8 @@ var ToolRegistry = class _ToolRegistry {
43770
46554
  }
43771
46555
  clear() {
43772
46556
  this.tools.clear();
46557
+ this.descriptionModes.clear();
46558
+ this._version++;
43773
46559
  }
43774
46560
  /**
43775
46561
  * Return a new ToolRegistry with the same registered tools and owners.
@@ -43777,6 +46563,9 @@ var ToolRegistry = class _ToolRegistry {
43777
46563
  */
43778
46564
  clone() {
43779
46565
  const copy = new _ToolRegistry();
46566
+ for (const [name, mode] of this.descriptionModes) {
46567
+ copy.descriptionModes.set(name, mode);
46568
+ }
43780
46569
  for (const { tool, owner } of this.listWithOwner()) copy.register(tool, owner);
43781
46570
  return copy;
43782
46571
  }
@@ -45138,6 +47927,12 @@ var PhaseOrchestrator = class {
45138
47927
  async executeSingleTask(task, phase) {
45139
47928
  const tracker = this.getTrackerForPhase(phase);
45140
47929
  tracker.updateNodeStatus(task.id, "in_progress");
47930
+ this.emit("phase.taskStarted", {
47931
+ phaseId: phase.id,
47932
+ taskId: task.id,
47933
+ taskTitle: task.title,
47934
+ agentName: task.assignee
47935
+ });
45141
47936
  const handle = this.phaseWorktrees.get(phase.id);
45142
47937
  return this.ctx.executeTask(task, phase.id, { cwd: handle?.dir, branch: handle?.branch });
45143
47938
  }
@@ -45342,6 +48137,92 @@ var PhaseOrchestrator = class {
45342
48137
  phase.assignedAgents = phase.assignedAgents.filter((id) => id !== agentId);
45343
48138
  this.emit("agent.released", { phaseId, agentId });
45344
48139
  }
48140
+ // ─── Interactive board mutations ──────────────────────────────────────────
48141
+ //
48142
+ // These are driven by an interactive board (WebUI/TUI), not the autonomous
48143
+ // loop. Each mutates the live graph, emits a typed event so every surface
48144
+ // stays in sync, and bumps updatedAt so the host re-persists.
48145
+ /** Find the phase whose task graph currently holds `taskId`. */
48146
+ findPhaseOfTask(taskId) {
48147
+ for (const phase of this.graph.phases.values()) {
48148
+ if (phase.taskGraph.nodes.has(taskId)) return phase;
48149
+ }
48150
+ return void 0;
48151
+ }
48152
+ /**
48153
+ * Move a task to another phase's task graph. Edges that referenced the task
48154
+ * are dropped (cross-phase dependencies are not modeled). No-op when the task
48155
+ * or target phase is missing, or it is already in the target phase.
48156
+ */
48157
+ moveTask(taskId, toPhaseId) {
48158
+ const from = this.findPhaseOfTask(taskId);
48159
+ const to = this.graph.phases.get(toPhaseId);
48160
+ if (!from || !to || from.id === toPhaseId) return false;
48161
+ const node = from.taskGraph.nodes.get(taskId);
48162
+ if (!node) return false;
48163
+ from.taskGraph.nodes.delete(taskId);
48164
+ from.taskGraph.rootNodes = from.taskGraph.rootNodes.filter((id) => id !== taskId);
48165
+ from.taskGraph.edges = from.taskGraph.edges.filter((e) => e.from !== taskId && e.to !== taskId);
48166
+ from.taskGraph.updatedAt = Date.now();
48167
+ node.parentId = void 0;
48168
+ node.children = void 0;
48169
+ node.updatedAt = Date.now();
48170
+ to.taskGraph.nodes.set(taskId, node);
48171
+ to.taskGraph.rootNodes.push(taskId);
48172
+ to.taskGraph.updatedAt = Date.now();
48173
+ this.trackerCache.delete(from.id);
48174
+ this.trackerCache.delete(to.id);
48175
+ this.graph.updatedAt = Date.now();
48176
+ this.emit("phase.taskMoved", { taskId, fromPhaseId: from.id, toPhaseId });
48177
+ return true;
48178
+ }
48179
+ /** (Re)assign a task to a specific agent (or clear with agentName/agentId omitted). */
48180
+ setTaskAssignee(taskId, agentId, agentName) {
48181
+ const phase = this.findPhaseOfTask(taskId);
48182
+ if (!phase) return false;
48183
+ const tracker = this.getTrackerForPhase(phase);
48184
+ tracker.updateNode(taskId, { assignee: agentName ?? agentId ?? "" });
48185
+ this.graph.updatedAt = Date.now();
48186
+ this.emit("phase.taskAssigned", { phaseId: phase.id, taskId, agentId, agentName });
48187
+ return true;
48188
+ }
48189
+ /** Add a new task to a phase. Returns the created task id, or null if the phase is missing. */
48190
+ addTask(phaseId, spec) {
48191
+ const phase = this.graph.phases.get(phaseId);
48192
+ if (!phase) return null;
48193
+ const tracker = this.getTrackerForPhase(phase);
48194
+ const node = tracker.addNode({
48195
+ title: spec.title,
48196
+ description: spec.description ?? "",
48197
+ type: spec.type ?? "feature",
48198
+ priority: spec.priority ?? "medium",
48199
+ status: "pending"
48200
+ });
48201
+ this.graph.updatedAt = Date.now();
48202
+ this.emit("phase.taskAdded", { phaseId, taskId: node.id, taskTitle: node.title });
48203
+ return node.id;
48204
+ }
48205
+ /**
48206
+ * Requeue a task to `pending` (clearing its retry counter) and nudge a
48207
+ * terminal/paused phase back to `ready` so the loop re-runs it. Backs both the
48208
+ * board's "retry" and "start" affordances.
48209
+ */
48210
+ requeueTask(taskId) {
48211
+ const phase = this.findPhaseOfTask(taskId);
48212
+ if (!phase) return false;
48213
+ const tracker = this.getTrackerForPhase(phase);
48214
+ tracker.updateNodeStatus(taskId, "pending");
48215
+ this.taskRetryCounts.delete(`${phase.id}:${taskId}`);
48216
+ if (phase.status === "completed" || phase.status === "failed" || phase.status === "paused") {
48217
+ this.graph.failedPhaseIds = this.graph.failedPhaseIds.filter((id) => id !== phase.id);
48218
+ this.graph.completedPhaseIds = this.graph.completedPhaseIds.filter((id) => id !== phase.id);
48219
+ this.graph.activePhaseIds = this.graph.activePhaseIds.filter((id) => id !== phase.id);
48220
+ this.phaseWorktrees.delete(phase.id);
48221
+ this.updatePhaseStatus(phase, "pending");
48222
+ }
48223
+ this.graph.updatedAt = Date.now();
48224
+ return true;
48225
+ }
45345
48226
  // ─── Events ───────────────────────────────────────────────────────────────
45346
48227
  emit(event, payload) {
45347
48228
  this.events.emit(event, payload);
@@ -46067,346 +48948,6 @@ var CheckpointManager = class {
46067
48948
  }
46068
48949
  }
46069
48950
  };
46070
- var MAX_SLUG = 40;
46071
- var WorktreeManager = class {
46072
- projectRoot;
46073
- events;
46074
- gitBin;
46075
- runGit;
46076
- /** Keyed by ownerId. */
46077
- handles = /* @__PURE__ */ new Map();
46078
- usedSlugs = /* @__PURE__ */ new Set();
46079
- constructor(opts) {
46080
- this.projectRoot = resolve(opts.projectRoot);
46081
- this.events = opts.events;
46082
- this.gitBin = opts.gitBin ?? "git";
46083
- this.runGit = opts.run ?? ((args, cwd) => this.defaultRun(args, cwd));
46084
- }
46085
- /** Create a fresh worktree + branch forked from the current base branch. */
46086
- async allocate(ownerId, opts = {}) {
46087
- const existing = this.handles.get(ownerId);
46088
- if (existing && (existing.status === "allocating" || existing.status === "active")) {
46089
- return existing;
46090
- }
46091
- const slug = this.makeSlug(opts.slugHint ?? ownerId);
46092
- const branch = `wstack/ap/${slug}`;
46093
- const dir = join(this.worktreesRoot(), slug);
46094
- const absDir = resolve(dir);
46095
- const absRoot = resolve(this.projectRoot);
46096
- if (!absDir.startsWith(absRoot + sep)) {
46097
- throw new Error(`Worktree dir "${absDir}" resolves outside project root`);
46098
- }
46099
- const baseBranch = opts.baseBranch ?? await this.detectBaseBranch();
46100
- const handle = {
46101
- id: slug,
46102
- ownerId,
46103
- ownerLabel: opts.ownerLabel ?? opts.slugHint ?? ownerId,
46104
- slug,
46105
- dir,
46106
- branch,
46107
- baseBranch,
46108
- status: "allocating",
46109
- createdAt: Date.now(),
46110
- updatedAt: Date.now(),
46111
- insertions: 0,
46112
- deletions: 0,
46113
- files: 0
46114
- };
46115
- this.handles.set(ownerId, handle);
46116
- try {
46117
- await mkdir(this.worktreesRoot(), { recursive: true });
46118
- const res = await this.runGit(
46119
- ["worktree", "add", "-b", branch, dir, baseBranch],
46120
- this.projectRoot
46121
- );
46122
- if (res.code !== 0) {
46123
- return this.fail(handle, res.stderr || "git worktree add failed");
46124
- }
46125
- } catch (err) {
46126
- return this.fail(handle, toErrorMessage(err));
46127
- }
46128
- this.setStatus(handle, "active");
46129
- this.emit("worktree.allocated", {
46130
- handleId: handle.id,
46131
- ownerId: handle.ownerId,
46132
- ownerLabel: handle.ownerLabel,
46133
- slug: handle.slug,
46134
- dir: handle.dir,
46135
- branch: handle.branch,
46136
- baseBranch: handle.baseBranch
46137
- });
46138
- return handle;
46139
- }
46140
- /** Stage everything and commit inside the worktree. */
46141
- async commitAll(handle, message) {
46142
- this.setStatus(handle, "committing");
46143
- await this.runGit(["add", "-A"], handle.dir);
46144
- const staged = await this.runGit(["diff", "--cached", "--quiet"], handle.dir);
46145
- if (staged.code === 0) {
46146
- this.emitCommitted(handle, false);
46147
- return { committed: false };
46148
- }
46149
- const idArgs = await this.identityArgs(handle.dir);
46150
- const committed = await this.runGit([...idArgs, "commit", "-m", message], handle.dir);
46151
- if (committed.code !== 0) {
46152
- this.fail(handle, committed.stderr || "git commit failed");
46153
- return { committed: false };
46154
- }
46155
- const stats = await this.collectStats(handle.dir);
46156
- handle.insertions = stats.insertions;
46157
- handle.deletions = stats.deletions;
46158
- handle.files = stats.files;
46159
- handle.sha = stats.sha;
46160
- handle.updatedAt = Date.now();
46161
- this.emitCommitted(handle, true);
46162
- return { committed: true };
46163
- }
46164
- /** Merge the worktree branch back into the base branch (squash by default). */
46165
- async merge(handle, opts = {}) {
46166
- const squash = opts.squash ?? true;
46167
- this.setStatus(handle, "merging");
46168
- const checkout = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
46169
- if (checkout.code !== 0) {
46170
- this.fail(handle, checkout.stderr || `checkout ${handle.baseBranch} failed`);
46171
- return { ok: false, stderr: checkout.stderr };
46172
- }
46173
- const mergeArgs = squash ? ["merge", "--squash", handle.branch] : ["merge", "--no-ff", handle.branch];
46174
- const merged = await this.runGit(mergeArgs, this.projectRoot);
46175
- if (merged.code !== 0) {
46176
- const fromOutput = parseConflictPaths(`${merged.stdout}
46177
- ${merged.stderr}`);
46178
- const fromIndex = await this.unmergedFiles();
46179
- const conflictFiles = [.../* @__PURE__ */ new Set([...fromOutput, ...fromIndex])];
46180
- if (opts.resolve) {
46181
- const finalized = await this.tryResolveConflict(handle, conflictFiles, opts);
46182
- if (finalized) return finalized;
46183
- }
46184
- await this.runGit(["reset", "--hard", "HEAD"], this.projectRoot);
46185
- handle.conflictFiles = conflictFiles;
46186
- this.setStatus(handle, "needs-review", { lastError: merged.stderr });
46187
- this.emit("worktree.conflict", {
46188
- handleId: handle.id,
46189
- ownerId: handle.ownerId,
46190
- branch: handle.branch,
46191
- conflictFiles
46192
- });
46193
- return { ok: false, conflict: true, conflictFiles, stderr: merged.stderr };
46194
- }
46195
- if (squash) {
46196
- const msg = opts.message ?? `merge ${handle.branch} (squash)`;
46197
- const idArgs = await this.identityArgs(this.projectRoot);
46198
- const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
46199
- if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
46200
- this.fail(handle, commit.stderr || "squash commit failed");
46201
- return { ok: false, stderr: commit.stderr };
46202
- }
46203
- }
46204
- this.setStatus(handle, "merged");
46205
- this.emit("worktree.merged", {
46206
- handleId: handle.id,
46207
- ownerId: handle.ownerId,
46208
- branch: handle.branch,
46209
- baseBranch: handle.baseBranch,
46210
- squash
46211
- });
46212
- return { ok: true };
46213
- }
46214
- /**
46215
- * Run the caller-supplied resolver against a conflicted squash-merge, then
46216
- * commit if it cleared every marker. Returns a successful `MergeResult` on a
46217
- * clean resolution, or `null` to signal the caller should fall back to the
46218
- * abort path. Never leaves the base tree committed-but-dirty: a partial or
46219
- * failed resolution returns `null` and the caller hard-resets.
46220
- */
46221
- async tryResolveConflict(handle, conflictFiles, opts) {
46222
- let resolved = false;
46223
- try {
46224
- resolved = opts.resolve ? await opts.resolve({ conflictFiles, cwd: this.projectRoot }) : false;
46225
- } catch {
46226
- resolved = false;
46227
- }
46228
- if (!resolved) return null;
46229
- await this.runGit(["add", "-A"], this.projectRoot);
46230
- if (await this.hasConflictMarkers()) return null;
46231
- const idArgs = await this.identityArgs(this.projectRoot);
46232
- const msg = opts.message ?? `merge ${handle.branch} (squash, conflict resolved)`;
46233
- const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
46234
- if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
46235
- return null;
46236
- }
46237
- handle.conflictFiles = conflictFiles;
46238
- this.setStatus(handle, "merged");
46239
- this.emit("worktree.merged", {
46240
- handleId: handle.id,
46241
- ownerId: handle.ownerId,
46242
- branch: handle.branch,
46243
- baseBranch: handle.baseBranch,
46244
- squash: true
46245
- });
46246
- return { ok: true, resolved: true, conflictFiles };
46247
- }
46248
- /**
46249
- * True when staged content still carries conflict markers. `git diff --cached
46250
- * --check` exits nonzero and prints a "leftover conflict marker" line for each
46251
- * survivor; whitespace-only errors (also flagged by --check) are ignored so a
46252
- * clean resolution with unrelated whitespace is not rejected.
46253
- */
46254
- async hasConflictMarkers() {
46255
- const check = await this.runGit(["diff", "--cached", "--check"], this.projectRoot);
46256
- if (check.code === 0) return false;
46257
- return /conflict marker/i.test(`${check.stdout}
46258
- ${check.stderr}`);
46259
- }
46260
- /**
46261
- * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`)
46262
- * are left on disk for inspection.
46263
- */
46264
- async release(handle, opts = {}) {
46265
- const keep = opts.keep || handle.status === "needs-review" || handle.status === "failed";
46266
- if (!keep) {
46267
- await this.runGit(["worktree", "remove", "--force", handle.dir], this.projectRoot);
46268
- await this.runGit(["branch", "-D", handle.branch], this.projectRoot);
46269
- await this.runGit(["worktree", "prune"], this.projectRoot);
46270
- this.handles.delete(handle.ownerId);
46271
- }
46272
- this.emit("worktree.released", {
46273
- handleId: handle.id,
46274
- ownerId: handle.ownerId,
46275
- branch: handle.branch,
46276
- kept: keep
46277
- });
46278
- }
46279
- get(ownerId) {
46280
- return this.handles.get(ownerId);
46281
- }
46282
- list() {
46283
- return [...this.handles.values()];
46284
- }
46285
- // ── internals ────────────────────────────────────────────────────────────
46286
- worktreesRoot() {
46287
- return join(this.projectRoot, ".wrongstack", "worktrees");
46288
- }
46289
- async detectBaseBranch() {
46290
- const head = await this.runGit(["rev-parse", "--abbrev-ref", "HEAD"], this.projectRoot);
46291
- const name = head.stdout.trim();
46292
- if (name && name !== "HEAD") return name;
46293
- const sha = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
46294
- return sha.stdout.trim() || "HEAD";
46295
- }
46296
- makeSlug(hint) {
46297
- let base = hint.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+/, "").replace(/[-.]+$/, "").slice(0, MAX_SLUG).replace(/[-.]+$/, "");
46298
- if (!base) base = "wt";
46299
- let slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
46300
- while (this.usedSlugs.has(slug)) slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
46301
- this.usedSlugs.add(slug);
46302
- return slug;
46303
- }
46304
- async collectStats(dir) {
46305
- const sha = (await this.runGit(["rev-parse", "HEAD"], dir)).stdout.trim();
46306
- const numstat = await this.runGit(["show", "--numstat", "--format=", "HEAD"], dir);
46307
- let insertions = 0;
46308
- let deletions = 0;
46309
- let files = 0;
46310
- for (const line of numstat.stdout.split("\n")) {
46311
- const m = line.trim().match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
46312
- if (!m) continue;
46313
- files++;
46314
- if (m[1] !== "-") insertions += Number(m[1]);
46315
- if (m[2] !== "-") deletions += Number(m[2]);
46316
- }
46317
- return { insertions, deletions, files, sha };
46318
- }
46319
- /**
46320
- * `git -c user.*` fallback so commits succeed on machines and CI runners
46321
- * that have no global git identity configured. Returns `[]` when both
46322
- * `user.name` and `user.email` are already set (the common case), so a real
46323
- * user's identity is never overridden. The worktree branch commits are
46324
- * squashed away on merge, so the fallback identity never reaches the base
46325
- * branch history.
46326
- */
46327
- async identityArgs(cwd) {
46328
- const name = (await this.runGit(["config", "user.name"], cwd)).stdout.trim();
46329
- const email = (await this.runGit(["config", "user.email"], cwd)).stdout.trim();
46330
- if (name && email) return [];
46331
- return [
46332
- "-c",
46333
- `user.name=${name || "AutoPhase"}`,
46334
- "-c",
46335
- `user.email=${email || "autophase@agent.local"}`
46336
- ];
46337
- }
46338
- async unmergedFiles() {
46339
- const res = await this.runGit(["diff", "--name-only", "--diff-filter=U"], this.projectRoot);
46340
- return res.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
46341
- }
46342
- emitCommitted(handle, committed) {
46343
- this.emit("worktree.committed", {
46344
- handleId: handle.id,
46345
- ownerId: handle.ownerId,
46346
- branch: handle.branch,
46347
- committed,
46348
- insertions: handle.insertions,
46349
- deletions: handle.deletions,
46350
- files: handle.files,
46351
- sha: handle.sha
46352
- });
46353
- }
46354
- fail(handle, error) {
46355
- this.setStatus(handle, "failed", { lastError: error });
46356
- this.emit("worktree.failed", {
46357
- handleId: handle.id,
46358
- ownerId: handle.ownerId,
46359
- branch: handle.branch,
46360
- error
46361
- });
46362
- return handle;
46363
- }
46364
- setStatus(handle, status, patch) {
46365
- handle.status = status;
46366
- handle.updatedAt = Date.now();
46367
- if (patch) Object.assign(handle, patch);
46368
- }
46369
- emit(event, payload) {
46370
- this.events?.emit(event, payload);
46371
- }
46372
- defaultRun(args, cwd) {
46373
- return new Promise((res) => {
46374
- let stdout = "";
46375
- let stderr = "";
46376
- const MAX_GIT_OUTPUT = 1e6;
46377
- const child = spawn(this.gitBin, args, {
46378
- cwd,
46379
- env: buildChildEnv(),
46380
- stdio: ["ignore", "pipe", "pipe"],
46381
- signal: AbortSignal.timeout(3e4),
46382
- windowsHide: true
46383
- });
46384
- child.stdout?.on("data", (c) => {
46385
- if (stdout.length < MAX_GIT_OUTPUT) stdout += c.toString();
46386
- });
46387
- child.stderr?.on("data", (c) => {
46388
- if (stderr.length < MAX_GIT_OUTPUT) stderr += c.toString();
46389
- });
46390
- child.on("error", (err) => res({ code: 1, stdout, stderr: err.message }));
46391
- child.on("close", (code) => res({ code: code ?? 1, stdout, stderr }));
46392
- });
46393
- }
46394
- };
46395
- function parseConflictPaths(output) {
46396
- const paths = /* @__PURE__ */ new Set();
46397
- for (const line of output.split("\n")) {
46398
- const m = line.match(/^CONFLICT \([^)]*\): Merge conflict in (.+?)\s*$/);
46399
- if (m?.[1]) paths.add(m[1]);
46400
- }
46401
- return [...paths];
46402
- }
46403
- function assertSafePath(dir, projectRoot) {
46404
- const root = resolve(projectRoot);
46405
- const abs = resolve(dir);
46406
- if (abs !== root && !abs.startsWith(root + sep)) {
46407
- throw new Error(`worktree path escapes project root: ${dir}`);
46408
- }
46409
- }
46410
48951
 
46411
48952
  // src/coordination/collab-bus.ts
46412
48953
  var CollaborationBus = class {
@@ -46905,8 +49446,8 @@ function extractManifestPath(msg) {
46905
49446
  }
46906
49447
  return void 0;
46907
49448
  }
46908
- function isManifestFile(path51) {
46909
- const name = pathBasename(path51).toLowerCase();
49449
+ function isManifestFile(path52) {
49450
+ const name = pathBasename(path52).toLowerCase();
46910
49451
  const manifests = [
46911
49452
  "package.json",
46912
49453
  "package-lock.json",
@@ -48416,6 +50957,6 @@ function createChimeraPlugin() {
48416
50957
  };
48417
50958
  }
48418
50959
 
48419
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, AdaptiveConcurrencyController, Agent, AgentError, AgentMonitorService, AgentStatusTracker, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousBrain, AutonomousCoordinator, AutonomousRunner, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CHIMERA_REVIEW_PROMPT, CONTEXT_WINDOW_MODES, CORE_RECONSTRUCT_EVENTS, ChangeManager, CheckpointManager, CloudSync, CollabSession, CollaborationBus, ConfigError, ConfigMigrationError, ConsensusProtocol, Container, Context, ConversationState, DANGEROUS_FOR_SUBAGENTS, DECISION_TIMEOUT_MS, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CIRCUIT_BREAKER_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_HQ_REDACTION_POLICY, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_QUALITY_CHECKS, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DEFAULT_TUI_THINKING_WORD, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultAttachmentStore, DefaultBrainArbiter, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMailbox, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorAlertLevel, DirectorStateCheckpoint, DoneConditionChecker, ENHANCER_SYSTEM_PROMPT, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FORBIDDEN_PROTO_KEYS, FileMemoryBackend, FleetBus, FleetCostCapError, FleetManager, FleetNotifier, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, GlobalMailbox, GraphMemoryBackend, HEAVY_BUDGET, HQ_AUTH_FILE_VERSION, HQ_PROTOCOL_VERSION, HookRegistry, HookRunner, HqPublisher, HumanEscalatingBrainArbiter, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, KNOWLEDGE_AGENTS, KnowledgeGraph, LAYER_1_IDENTITY, LIGHT_BUDGET, LLMSelector, LargeAnswerStore, MATRIX_PHASE_KEYS, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, MAX_TUI_THINKING_WORD_LENGTH, MEDIUM_BUDGET, MEMORY_TYPE_LABELS, META_AGENTS, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, ObservableBrainArbiter, PLANNING_AGENTS, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, STANDARD_AUDIT_EVENTS, ScopedEventBus, SddError, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionMemoryConsolidator, SessionRecovery, SessionRegistry, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, StreamHangError, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TOKENS, TaskAuctioneer, TaskDAG, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolCapabilities, ToolError, ToolErrorCategory, ToolExecutor, ToolRegistry, VERIFY_AGENTS, WIDE_SUBAGENT_CAPABILITIES, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyModelRuntime, applyRosterBudget, asBlocks, asText, assertNever, assertNotPrivateHost, assertSafePath, assessCommitSafety, atomicWrite, attachAutoExtend, attachDepWatcherBridge, attachMailboxChecker, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, bootConfig, braveSearchServer, buildBtwBlock, buildChildEnv, buildContextEvidenceDigest, buildGoalPreamble, buildLosslessDigest, buildMailboxBlock, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildQueuedMessagesBlock, buildRecoveryStrategies, buildSmartDigest, buildTranscriptFromEvents, classifyFamily, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compactLog, compactSchemaDescriptions, compactToolDefinitionForWire, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeMessageTokens, computeTaskItemProgress, computeTaskProgress, consumeBtwNotes, consumeQueuedMessagesUpdate, context7Server, contextManagerTool, createAgentMonitorService, createAutoExecutor, createAutoPhaseFromTaskGraph, createAutonomyBrain, createChimeraPlugin, createContextEvidenceState, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createGlobalMailbox, createHqEventEnvelope, createHqPublisherFromEnv, createMailboxChecker, createMailboxEventPayload, createMailboxHooks, createMailboxSnapshotPayload, createMailboxSnapshotPayloadFromMailbox, createMcpControlTool, createMcpUseTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSessionEventBridge, createSkillsPlugin, createStrategyCompactor, createSyncPlugin, createTieredBrainArbiter, createToolOutputSerializer, decryptConfigSecrets, deepMerge, defaultGitignoreUpdater, defaultHqDataDir, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, definePlugin, deriveTodosFromPlanItem, describeCatalogModel, detectEcosystem, detectNewlineStyle, detectEcosystem as detectPackageEcosystem, dispatchAgent, downloadGitHubTarball, eliseOldToolResults, emptyGoal, emptyHqAuthFile, emptyPlan, emptyTaskFile, encryptConfigSecrets, encryptedPrefixForVersion, enhanceUserPrompt, ensureDir, ensureHqFirstRunAuthFile, escapeGlobSubject, estimateMessageTokens, estimateMessages, estimateRequestTokens, estimateRequestTokensCalibrated, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, expandGlob, expandIPv6, expectDefined, extractRunEnv, extractText, filesystemServer, findCriticalPath, findPreserveStart, flagsToConfigPatch, formatContextWindowModeList, formatDecisionSummary, formatGoal, formatHumanPrompt, formatPlan, formatPlanTemplates, formatTaskList, formatTaskProgress, formatTodosList, gatedEnhancerReasoning, generateSessionId, getAgentDefinition, getCalibrationState, getContextWindowMode, getDangerousCapabilities, getFileHistory, getFilesByAgent, getFullLog, getFullPackageLog, getJsonPath, getLastAuthor, getManifestPackages, getPackageAuthor, getPackagesByAgent, getPlanTemplate, getSessionRegistry, getTemplate, getTermSize, githubServer, goalFilePath, googleMapsServer, hasCapability, hasDangerousCapabilityForSubagents, hasSessionRegistry, hasTextContent, hashRequest, hookMatcherMatches, hqAuthFilePath, hqRuntimeFilePath, injectPendingMailboxMessages, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isInteractive, isJsonObject, isPathSubjectKey, isPluginError, isPrimitiveArray, isPrivateIPv4, isPrivateIPv6, isSddError, isSecretField, isSessionError, isStdinTTY, isStdoutTTY, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isValidMatrixKey, isWrongStackError, jsonObjectFileExists, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTasks, loadTodosCheckpoint, loadUserModes, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, mapMailboxAgentToHqSummary, mapMailboxMessageToHqSummary, mapSessionEventToEntries, markAssistantReferencedEvidence, matchAny, matchGlob, matrixKeyKind, mergeCustomModelDefs, mergeModelsPayload, mergeToolResults, migratePlaintextSecrets, miniMaxVisionServer, mintHqBrowserToken, mintHqToken, mutateHqAuthFile, mutatePlan, mutateTasks, noOpLogger, noOpVault, normalizePathSubject, normalizeRecipient, normalizeToLf, normalizeTokenSavingTier, normalizeTuiThinkingWord, normalizedEqual, onResize, parseContinueDirective, parseEncryptedVersion, parseEntries, parseHqEventPayload, parseHqFrame, parseProgressFromText, parseSkillRef, peekQueuedMessages, pendingBtwCount, phaseForRole, playwrightServer, projectHash, projectSlug, readHqAuthFile, readHqRuntimeFileSync, readJsonObjectFile, recentTextTurns, recordActualUsage, recordFileAction, recordPackageAction, recordProgress, recordToolOutputEvidence, recordUserIntentEvidence, redactHqEvent, redactHqValue, removeJsonPath, removeJsonPathInFile, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, repeatedReadPressure, resetCalibration, resolveAuditLevel, resolveCacheForRequest, resolveChimeraConfig, resolveContextWindowPolicy, resolveHqConfig, resolveHqConfigFromEnv, resolveHqDataDir, resolveMailboxIdentity, resolveModelMatrix, resolveModelRuntime, resolveProjectDir, resolveProviderModelList, resolveReasoningForRequest, resolveSessionLoggingConfig, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, rotateConfigKeys, runConfigMigrations, runProviderWithRetry, runShellHook, safeParse, safeStringify, sanitizeJsonString, sanitizeModel, sanitizeNodeOptions, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, scoreAgents, scoreMessage, scrubAndTruncateHqPreview, securitySlashCommand, sentinelServer, setBtwNote, setJsonPath, setJsonPathInFile, setOutputLineGuard, setPlanItemStatus, setProgress, setQueuedMessagesSnapshot, setRawMode, shouldEnhance, slackServer, sleep, sshManagerServer, stableStringify, startAgentMonitorEventBridge, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, startPackageOutdatedWatcher, startSessionTelemetryBridge, startTechStackConsumer, stripAnsi, subjectForToolInput, summarizeHqToolArgs, summarizeUsage, templateToMarkdown, toErrorMessage, toStyle, toWrongStackError, topologicalSort, truncate, unifiedDiff, unloadPlugins, updateJsonObjectFile, updatePackageOutdatedStatus, validateAgainstSchema, watchHqAuthFile, wireMetricsToEvents, withDisabledToolFiltering, withFileLock, wrapAsState, writeErr, writeHqAuthFile, writeHqRuntimeFile, writeJsonObjectFile, writeOut, wstackGlobalRoot, zaiVisionServer };
50960
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, AdaptiveConcurrencyController, Agent, AgentError, AgentMonitorService, AgentStatusTracker, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousBrain, AutonomousCoordinator, AutonomousRunner, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CHIMERA_REVIEW_PROMPT, CONTEXT_WINDOW_MODES, CORE_RECONSTRUCT_EVENTS, ChangeManager, CheckpointManager, CloudSync, CollabSession, CollaborationBus, ConfigError, ConfigMigrationError, ConsensusProtocol, Container, Context, ConversationState, DANGEROUS_FOR_SUBAGENTS, DECISION_TIMEOUT_MS, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CIRCUIT_BREAKER_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_HQ_REDACTION_POLICY, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_QUALITY_CHECKS, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DEFAULT_TOOL_DESCRIPTION_MODE, DEFAULT_TUI_THINKING_WORD, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultAttachmentStore, DefaultBrainArbiter, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMailbox, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorAlertLevel, DirectorStateCheckpoint, DoneConditionChecker, ENHANCER_SYSTEM_PROMPT, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FORBIDDEN_PROTO_KEYS, FileMemoryBackend, FleetBus, FleetCostCapError, FleetManager, FleetNotifier, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, GlobalMailbox, GraphMemoryBackend, HEAVY_BUDGET, HQ_AUTH_FILE_VERSION, HQ_PROTOCOL_VERSION, HookRegistry, HookRunner, HqPublisher, HumanEscalatingBrainArbiter, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, KNOWLEDGE_AGENTS, KnowledgeGraph, LAYER_1_IDENTITY, LIGHT_BUDGET, LLMSelector, LargeAnswerStore, MATRIX_PHASE_KEYS, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, MAX_TUI_THINKING_WORD_LENGTH, MEDIUM_BUDGET, MEMORY_TYPE_LABELS, META_AGENTS, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, ObservableBrainArbiter, PLANNING_AGENTS, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, STANDARD_AUDIT_EVENTS, ScopedEventBus, SddBoardProjector, SddBoardStore, SddError, SddInterviewDriver, SddParallelRun, SddRunRegistry, SddSupervisor, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionMemoryConsolidator, SessionRecovery, SessionRegistry, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, StreamHangError, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TOKENS, TaskAuctioneer, TaskDAG, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolCapabilities, ToolError, ToolErrorCategory, ToolExecutor, ToolRegistry, VERIFY_AGENTS, WIDE_SUBAGENT_CAPABILITIES, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyModelRuntime, applyRosterBudget, applyToolDescriptionModeToTool, applyToolDescriptionModes, asBlocks, asText, assertNever, assertNotPrivateHost, assertSafePath, assessCommitSafety, assignNickname, atomicWrite, attachAutoExtend, attachDepWatcherBridge, attachMailboxChecker, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, bootConfig, braveSearchServer, buildBoardSnapshot, buildBoardTasks, buildBtwBlock, buildChildEnv, buildContextEvidenceDigest, buildGoalPreamble, buildLosslessDigest, buildMailboxBlock, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildQueuedMessagesBlock, buildRecoveryStrategies, buildSmartDigest, buildTranscriptFromEvents, classifyFamily, cleanupSddWorktrees, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compactLog, compactSchemaDescriptions, compactToolDefinitionForWire, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeMessageTokens, computeTaskItemProgress, computeTaskProgress, consumeBtwNotes, consumeQueuedMessagesUpdate, context7Server, contextManagerTool, createAgentMonitorService, createAutoExecutor, createAutoPhaseFromTaskGraph, createAutonomyBrain, createChimeraPlugin, createContextEvidenceState, createContextManagerTool, createDefaultPipelines, createDelegateTool, createFallbackModelExtension, createGitPlugin, createGlobalMailbox, createHqEventEnvelope, createHqPublisherFromEnv, createMailboxChecker, createMailboxEventPayload, createMailboxHooks, createMailboxSnapshotPayload, createMailboxSnapshotPayloadFromMailbox, createMcpControlTool, createMcpUseTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSessionEventBridge, createSkillsPlugin, createStrategyCompactor, createSyncPlugin, createTieredBrainArbiter, createToolOutputSerializer, decryptConfigSecrets, deepMerge, defaultGitignoreUpdater, defaultHqDataDir, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, definePlugin, deriveTodosFromPlanItem, describeCatalogModel, destroySddProject, detectEcosystem, detectNewlineStyle, detectEcosystem as detectPackageEcosystem, dispatchAgent, downloadGitHubTarball, effectiveFallbackChain, eliseOldToolResults, emptyGoal, emptyHqAuthFile, emptyPlan, emptyTaskFile, encryptConfigSecrets, encryptedPrefixForVersion, enhanceUserPrompt, ensureDir, ensureHqFirstRunAuthFile, escapeGlobSubject, estimateMessageTokens, estimateMessages, estimateRequestTokens, estimateRequestTokensCalibrated, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, expandGlob, expandIPv6, expectDefined, extractRunEnv, extractText, extractVerificationCommand, filesystemServer, findCriticalPath, findPreserveStart, flagsToConfigPatch, formatContextWindowModeList, formatDecisionSummary, formatGoal, formatHumanPrompt, formatPlan, formatPlanTemplates, formatTaskList, formatTaskProgress, formatTodosList, gatedEnhancerReasoning, generateSessionId, getAgentDefinition, getCalibrationState, getContextWindowMode, getDangerousCapabilities, getFileHistory, getFilesByAgent, getFullLog, getFullPackageLog, getJsonPath, getLastAuthor, getManifestPackages, getPackageAuthor, getPackagesByAgent, getPlanTemplate, getSessionRegistry, getTemplate, getTermSize, getToolDescriptionMode, githubServer, goalFilePath, googleMapsServer, hasCapability, hasConflictMarkers, hasDangerousCapabilityForSubagents, hasSessionRegistry, hasTextContent, hashRequest, hookMatcherMatches, hqAuthFilePath, hqRuntimeFilePath, injectPendingMailboxMessages, isAgentError, isConfigError, isContextWindowModeId, isExplanatoryText, isFsError, isImageBlock, isInteractive, isJsonObject, isPathSubjectKey, isPluginError, isPrimitiveArray, isPrivateIPv4, isPrivateIPv6, isSddError, isSecretField, isSessionError, isStdinTTY, isStdoutTTY, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isValidMatrixKey, isWrongStackError, jsonObjectFileExists, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTasks, loadTodosCheckpoint, loadUserModes, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeCommandVerifier, makeContinueToNextIterationTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeLlmConflictResolver, makeLlmSubtaskGenerator, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makePreferSideConflictResolver, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, mapMailboxAgentToHqSummary, mapMailboxMessageToHqSummary, mapSessionEventToEntries, markAssistantReferencedEvidence, matchAny, matchGlob, matrixKeyKind, mergeCustomModelDefs, mergeModelsPayload, mergeToolResults, migratePlaintextSecrets, miniMaxVisionServer, mintHqBrowserToken, mintHqToken, mutateHqAuthFile, mutatePlan, mutateTasks, nicknameKeyFromDisplay, noOpLogger, noOpVault, normalizePathSubject, normalizeRecipient, normalizeToLf, normalizeTokenSavingTier, normalizeToolDescriptionMode, normalizeTuiThinkingWord, normalizedEqual, onResize, parseContinueDirective, parseEncryptedVersion, parseEntries, parseHqEventPayload, parseHqFrame, parseModelRef, parseProgressFromText, parseSkillRef, peekQueuedMessages, pendingBtwCount, phaseForRole, playwrightServer, projectHash, projectSlug, readHqAuthFile, readHqRuntimeFileSync, readJsonObjectFile, recentTextTurns, recordActualUsage, recordFileAction, recordPackageAction, recordProgress, recordToolOutputEvidence, recordUserIntentEvidence, redactHqEvent, redactHqValue, removeJsonPath, removeJsonPathInFile, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, repeatedReadPressure, resetCalibration, resolveAuditLevel, resolveCacheForRequest, resolveChimeraConfig, resolveConflictText, resolveContextWindowPolicy, resolveHqConfig, resolveHqConfigFromEnv, resolveHqDataDir, resolveMailboxIdentity, resolveModelMatrix, resolveModelRuntime, resolveProjectDir, resolveProviderModelList, resolveReasoningForRequest, resolveSessionLoggingConfig, resolveToolDescriptionMode, resolveWstackPaths, rewriteConfigEncrypted, rollbackSddRunFromDisk, rosterSummaryFromConfigs, rotateConfigKeys, runConfigMigrations, runProviderWithRetry, runShellHook, safeParse, safeStringify, sanitizeJsonString, sanitizeModel, sanitizeNodeOptions, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, scoreAgents, scoreMessage, scrubAndTruncateHqPreview, securitySlashCommand, sentinelServer, setBtwNote, setJsonPath, setJsonPathInFile, setOutputLineGuard, setPlanItemStatus, setProgress, setQueuedMessagesSnapshot, setRawMode, setToolDescriptionMode, shortIdMap, shouldEnhance, simplifyToolDescription, slackServer, sleep, smartDefaultFallbackChain, sshManagerServer, stableStringify, startAgentMonitorEventBridge, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, startPackageOutdatedWatcher, startSddRun, startSessionTelemetryBridge, startTechStackConsumer, stripAnsi, subjectForToolInput, summarizeHqToolArgs, summarizeUsage, templateToMarkdown, toErrorMessage, toStyle, toWrongStackError, topologicalSort, truncate, unifiedDiff, unloadPlugins, updateJsonObjectFile, updatePackageOutdatedStatus, validateAgainstSchema, watchHqAuthFile, wireMetricsToEvents, withDisabledToolFiltering, withFileLock, wrapAsState, writeErr, writeHqAuthFile, writeHqRuntimeFile, writeJsonObjectFile, writeOut, wstackGlobalRoot, zaiVisionServer };
48420
50961
  //# sourceMappingURL=index.js.map
48421
50962
  //# sourceMappingURL=index.js.map