@wrongstack/core 0.272.2 → 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
@@ -1,14 +1,15 @@
1
1
  import * as crypto2 from 'crypto';
2
2
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, scryptSync, createHash } from 'crypto';
3
3
  import * as fsp2 from 'fs/promises';
4
+ import { readFile, writeFile, mkdir } from 'fs/promises';
4
5
  import * as path4 from 'path';
5
- import { isAbsolute, resolve } from 'path';
6
+ import { isAbsolute, join, resolve, sep } from 'path';
6
7
  import * as fs4 from 'fs';
7
8
  import { createReadStream } from 'fs';
8
9
  import { createInterface } from 'readline';
9
10
  import * as os from 'os';
10
11
  import { hostname } from 'os';
11
- import { execFile } from 'child_process';
12
+ import { execFile, spawn } from 'child_process';
12
13
  import { promisify } from 'util';
13
14
  import { EventEmitter } from 'events';
14
15
 
@@ -107,7 +108,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
107
108
  if (Date.now() - started >= timeoutMs) {
108
109
  throw new Error(`Timed out waiting for file lock: ${targetPath}`);
109
110
  }
110
- await new Promise((resolve7) => setTimeout(resolve7, 25));
111
+ await new Promise((resolve8) => setTimeout(resolve8, 25));
111
112
  }
112
113
  }
113
114
  try {
@@ -140,7 +141,7 @@ async function renameWithRetry(from, to) {
140
141
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
141
142
  throw err;
142
143
  }
143
- await new Promise((resolve7) => setTimeout(resolve7, delays[i]));
144
+ await new Promise((resolve8) => setTimeout(resolve8, delays[i]));
144
145
  }
145
146
  }
146
147
  throw lastErr;
@@ -449,6 +450,126 @@ function assertNever(x, message) {
449
450
 
450
451
  // src/utils/index.ts
451
452
  init_atomic_write();
453
+
454
+ // src/utils/child-env.ts
455
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
456
+ "PATH",
457
+ "HOME",
458
+ "USER",
459
+ "USERNAME",
460
+ "LOGNAME",
461
+ "SHELL",
462
+ "LANG",
463
+ "LC_ALL",
464
+ "LC_CTYPE",
465
+ "TERM",
466
+ "TZ",
467
+ "TMPDIR",
468
+ "TEMP",
469
+ "TMP",
470
+ "PWD",
471
+ "OLDPWD",
472
+ "COMSPEC",
473
+ "SYSTEMROOT",
474
+ "SYSTEMDRIVE",
475
+ "WINDIR",
476
+ "PROGRAMFILES",
477
+ "PROGRAMFILES(X86)",
478
+ "PROGRAMDATA",
479
+ "APPDATA",
480
+ "LOCALAPPDATA",
481
+ "USERPROFILE",
482
+ "PUBLIC",
483
+ "PATHEXT"
484
+ ]);
485
+ var SECRET_NAME_PARTS = [
486
+ "TOKEN",
487
+ "SECRET",
488
+ "PASSWORD",
489
+ "PASSWD",
490
+ "AUTH",
491
+ "CRED",
492
+ "BEARER",
493
+ "COOKIE",
494
+ "PRIVATE"
495
+ ];
496
+ function looksSecret(name) {
497
+ const upper = name.toUpperCase();
498
+ for (const p of SECRET_NAME_PARTS) {
499
+ if (upper.includes(p)) return true;
500
+ }
501
+ if (/(?:^|_)KEY(?:$|_|S$)/i.test(upper)) return true;
502
+ if (/API[_-]?KEY/i.test(upper)) return true;
503
+ if (/ACCESS[_-]?KEY/i.test(upper)) return true;
504
+ if (/SESSION[_-]?ID/i.test(upper) === false && /SESSION/i.test(upper)) {
505
+ return true;
506
+ }
507
+ return false;
508
+ }
509
+ function valueHasEmbeddedCredential(value) {
510
+ return /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]*:[^/\s@]+@/i.test(value);
511
+ }
512
+ var NODE_OPTIONS_INJECTION_FLAG = /^(?:--require|-r|--import|--loader|--experimental-loader)$/;
513
+ var NODE_OPTIONS_INJECTION_FLAG_EQ = /^(?:--require|-r|--import|--loader|--experimental-loader)=/;
514
+ function sanitizeNodeOptions(value) {
515
+ const tokens = value.split(/\s+/).filter(Boolean);
516
+ const kept = [];
517
+ for (let i = 0; i < tokens.length; i++) {
518
+ const tok = tokens[i];
519
+ if (NODE_OPTIONS_INJECTION_FLAG_EQ.test(tok)) continue;
520
+ if (NODE_OPTIONS_INJECTION_FLAG.test(tok)) {
521
+ i++;
522
+ continue;
523
+ }
524
+ kept.push(tok);
525
+ }
526
+ return kept.join(" ");
527
+ }
528
+ function buildChildEnv(optsOrSessionId) {
529
+ const opts = {};
530
+ const hasOwn = Object.hasOwn(process.env, "WRONGSTACK_CHILD_ENV_PASSTHROUGH");
531
+ const legacyHasOwn = Object.hasOwn(process.env, "WRONGSTACK_BASH_ENV_PASSTHROUGH");
532
+ const passthrough = hasOwn && process.env["WRONGSTACK_CHILD_ENV_PASSTHROUGH"] === "1" || legacyHasOwn && process.env["WRONGSTACK_BASH_ENV_PASSTHROUGH"] === "1";
533
+ if (passthrough && !process.env["CI"]) {
534
+ console.warn(
535
+ "[agent] WARNING: WRONGSTACK_*_ENV_PASSTHROUGH=1 is active \u2014\n all parent env vars (including API keys) forwarded to child processes.\n Do not use on shared or multi-tenant systems."
536
+ );
537
+ }
538
+ const out = {};
539
+ const nodeEnvDefaulted = process.env["WRONGSTACK_NODE_ENV_DEFAULTED"] === "1";
540
+ for (const [k, v] of Object.entries(process.env)) {
541
+ if (v === void 0) continue;
542
+ if (nodeEnvDefaulted && (k === "NODE_ENV" || k === "WRONGSTACK_NODE_ENV_DEFAULTED")) continue;
543
+ if (passthrough) {
544
+ out[k] = v;
545
+ continue;
546
+ }
547
+ const upper = k.toUpperCase();
548
+ if (valueHasEmbeddedCredential(v)) continue;
549
+ if (ALLOWED_KEYS.has(upper)) {
550
+ out[k] = v;
551
+ continue;
552
+ }
553
+ if (looksSecret(upper)) continue;
554
+ if (upper === "NODE_OPTIONS") {
555
+ const sanitized = sanitizeNodeOptions(v);
556
+ if (sanitized) out[k] = sanitized;
557
+ continue;
558
+ }
559
+ if (upper.startsWith("NODE_") || upper.startsWith("NPM_") || upper.startsWith("PNPM_") || upper.startsWith("YARN_") || upper.startsWith("GIT_") || upper.startsWith("CI") || upper.startsWith("XDG_") || // Our own non-secret knobs (WRONGSTACK_HOME, WRONGSTACK_SESSION_ID, …).
560
+ // Secrets never live in WRONGSTACK_* env vars (they're in the encrypted
561
+ // vault). Forwarding keeps child wstack processes — e.g. ones spawned
562
+ // by the test suite — inside the same redirected global root.
563
+ upper.startsWith("WRONGSTACK_") || upper === "EDITOR" || upper === "VISUAL" || upper === "PAGER") {
564
+ out[k] = v;
565
+ }
566
+ }
567
+ if (opts.extra) {
568
+ Object.assign(out, opts.extra);
569
+ }
570
+ if (opts.sessionId) out["WRONGSTACK_SESSION_ID"] = opts.sessionId;
571
+ return out;
572
+ }
452
573
  var MAX_DIGEST_CHARS = 4e3;
453
574
  function createContextEvidenceState() {
454
575
  return {
@@ -882,11 +1003,11 @@ function validateAgainstSchema(value, schema) {
882
1003
  walk(value, schema, "", errors);
883
1004
  return { ok: errors.length === 0, errors };
884
1005
  }
885
- function walk(value, schema, path22, errors) {
1006
+ function walk(value, schema, path23, errors) {
886
1007
  if (schema.enum !== void 0) {
887
1008
  if (!schema.enum.some((e) => deepEqual(e, value))) {
888
1009
  errors.push({
889
- path: path22 || "<root>",
1010
+ path: path23 || "<root>",
890
1011
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
891
1012
  });
892
1013
  return;
@@ -895,7 +1016,7 @@ function walk(value, schema, path22, errors) {
895
1016
  if (typeof schema.type === "string") {
896
1017
  if (!checkType(value, schema.type)) {
897
1018
  errors.push({
898
- path: path22 || "<root>",
1019
+ path: path23 || "<root>",
899
1020
  message: `expected ${schema.type}, got ${describeType(value)}`
900
1021
  });
901
1022
  return;
@@ -905,20 +1026,20 @@ function walk(value, schema, path22, errors) {
905
1026
  const obj = value;
906
1027
  for (const req of schema.required ?? []) {
907
1028
  if (!(req in obj)) {
908
- errors.push({ path: joinPath(path22, req), message: "required property missing" });
1029
+ errors.push({ path: joinPath(path23, req), message: "required property missing" });
909
1030
  }
910
1031
  }
911
1032
  if (schema.properties) {
912
1033
  for (const [key, subSchema] of Object.entries(schema.properties)) {
913
1034
  if (key in obj) {
914
- walk(obj[key], subSchema, joinPath(path22, key), errors);
1035
+ walk(obj[key], subSchema, joinPath(path23, key), errors);
915
1036
  }
916
1037
  }
917
1038
  }
918
1039
  }
919
1040
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
920
1041
  for (let i = 0; i < value.length; i++) {
921
- walk(value[i], schema.items, `${path22}[${i}]`, errors);
1042
+ walk(value[i], schema.items, `${path23}[${i}]`, errors);
922
1043
  }
923
1044
  }
924
1045
  }
@@ -1088,7 +1209,7 @@ function safeParse(input, maxBytes = 5e6) {
1088
1209
 
1089
1210
  // src/utils/sleep.ts
1090
1211
  function sleep(ms) {
1091
- return new Promise((resolve7) => setTimeout(resolve7, ms));
1212
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
1092
1213
  }
1093
1214
 
1094
1215
  // src/utils/string.ts
@@ -2079,6 +2200,7 @@ function resolveWstackPaths(opts) {
2079
2200
  projectSddSession: path4.join(projectDir, "sdd-session.json"),
2080
2201
  projectPlan: path4.join(projectDir, "plan.json"),
2081
2202
  projectAutophase: path4.join(projectDir, "autophase"),
2203
+ projectSddBoards: path4.join(projectDir, "sdd-boards"),
2082
2204
  syncConfig: path4.join(globalRoot, "sync.json"),
2083
2205
  projectStatus: (projectHash2) => path4.join(globalRoot, "projects", projectHash2, "status.json")
2084
2206
  };
@@ -3429,7 +3551,6 @@ var FileSessionWriter = class _FileSessionWriter {
3429
3551
  }
3430
3552
  const writeFd = await fsp2.open(tmpPath, "w", 384);
3431
3553
  try {
3432
- let copied = 0;
3433
3554
  let readOffset = 0;
3434
3555
  while (readOffset < newlineAfterCheckpoint) {
3435
3556
  const toCopy = Math.min(CHUNK_SIZE, newlineAfterCheckpoint - readOffset);
@@ -3438,7 +3559,6 @@ var FileSessionWriter = class _FileSessionWriter {
3438
3559
  if (r === 0) break;
3439
3560
  await writeFd.write(copyBuf, 0, r);
3440
3561
  readOffset += r;
3441
- copied += r;
3442
3562
  }
3443
3563
  const raw = await fsp2.readFile(this.filePath);
3444
3564
  const tail = raw.subarray(newlineAfterCheckpoint).toString("utf8");
@@ -4647,9 +4767,9 @@ ${body.trim()}`);
4647
4767
  if (!this.persistBackup || scope === "project-agents") return;
4648
4768
  try {
4649
4769
  const content = await this.backend.readAll(scope, this.files[scope]);
4650
- const { writeFile: writeFile5, mkdir: mkdir7 } = await import('fs/promises');
4651
- await mkdir7(this.backupDir, { recursive: true });
4652
- await writeFile5(`${this.backupDir}/${scope}.md`, content, "utf8");
4770
+ const { writeFile: writeFile7, mkdir: mkdir8 } = await import('fs/promises');
4771
+ await mkdir8(this.backupDir, { recursive: true });
4772
+ await writeFile7(`${this.backupDir}/${scope}.md`, content, "utf8");
4653
4773
  } catch {
4654
4774
  }
4655
4775
  }
@@ -5397,6 +5517,7 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
5397
5517
  iterationTimeoutMs: 3e5,
5398
5518
  sessionTimeoutMs: 18e5,
5399
5519
  perIterationOutputCapBytes: 1e5,
5520
+ descriptionMode: Object.freeze({}),
5400
5521
  autoExtendLimit: true,
5401
5522
  restrictToProjectRoot: false
5402
5523
  });
@@ -5441,6 +5562,7 @@ var BEHAVIOR_DEFAULTS = {
5441
5562
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
5442
5563
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
5443
5564
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
5565
+ descriptionMode: DEFAULT_TOOLS_CONFIG.descriptionMode,
5444
5566
  autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
5445
5567
  restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
5446
5568
  },
@@ -7355,6 +7477,8 @@ var ToolCapabilities = {
7355
7477
  MCP_PROXY: "mcp.proxy",
7356
7478
  /** Can spawn or manage subagents / multi-agent tasks. */
7357
7479
  SUBAGENT_SPAWN: "subagent.spawn",
7480
+ /** Can inspect fleet/subagent coordination state without mutating it. */
7481
+ COORDINATION_FLEET_READ: "coordination.fleet.read",
7358
7482
  /** Can mutate global or session configuration / trust state. */
7359
7483
  CONFIG_MUTATE: "config.mutate",
7360
7484
  /** Can install packages or run package managers with side effects. */
@@ -8468,8 +8592,8 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
8468
8592
  });
8469
8593
  await Promise.race([
8470
8594
  drainPromise,
8471
- new Promise((resolve7) => {
8472
- drainTimer = setTimeout(resolve7, STREAM_DRAIN_TIMEOUT_MS);
8595
+ new Promise((resolve8) => {
8596
+ drainTimer = setTimeout(resolve8, STREAM_DRAIN_TIMEOUT_MS);
8473
8597
  })
8474
8598
  ]);
8475
8599
  } finally {
@@ -8576,7 +8700,7 @@ async function runProviderWithRetry(opts) {
8576
8700
  description
8577
8701
  });
8578
8702
  }
8579
- await new Promise((resolve7, reject) => {
8703
+ await new Promise((resolve8, reject) => {
8580
8704
  let settled = false;
8581
8705
  const cleanup = () => {
8582
8706
  clearTimeout(t);
@@ -8592,7 +8716,7 @@ async function runProviderWithRetry(opts) {
8592
8716
  if (settled) return;
8593
8717
  settled = true;
8594
8718
  cleanup();
8595
- resolve7();
8719
+ resolve8();
8596
8720
  }, delay);
8597
8721
  if (signal.aborted) {
8598
8722
  onAbort();
@@ -11505,8 +11629,8 @@ ${recentJournal}` : "No prior iterations.",
11505
11629
  await saveGoal(this.goalPath, abandoned, this.opts.events);
11506
11630
  }
11507
11631
  try {
11508
- const { unlink: unlink11 } = await import('fs/promises');
11509
- await unlink11(this.goalPath);
11632
+ const { unlink: unlink13 } = await import('fs/promises');
11633
+ await unlink13(this.goalPath);
11510
11634
  } catch {
11511
11635
  }
11512
11636
  this.opts.onEternalStop?.();
@@ -11892,13 +12016,13 @@ var SubagentBudget = class _SubagentBudget {
11892
12016
  if (!bus?.hasListenerFor("budget.threshold_reached")) {
11893
12017
  return Promise.resolve("stop");
11894
12018
  }
11895
- return new Promise((resolve7) => {
12019
+ return new Promise((resolve8) => {
11896
12020
  let resolved = false;
11897
12021
  const respond = (d) => {
11898
12022
  if (resolved) return;
11899
12023
  resolved = true;
11900
12024
  clearTimeout(fallback);
11901
- resolve7(d);
12025
+ resolve8(d);
11902
12026
  };
11903
12027
  const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
11904
12028
  bus.emit("budget.threshold_reached", {
@@ -15253,14 +15377,15 @@ var SHADOW_AGENT = {
15253
15377
  id: "shadow-agent",
15254
15378
  name: "Shadow",
15255
15379
  role: "shadow-agent",
15256
- prompt: `You are the Shadow Agent \u2014 a silent background monitor for the WrongStack fleet.
15380
+ prompt: `You are the Shadow Agent \u2014 a quiet, one-shot monitor for the WrongStack fleet.
15257
15381
 
15258
- Your job is to observe, detect anomalies, and be ready to intervene \u2014 but only when commanded.
15382
+ 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.
15259
15383
 
15260
15384
  ## Core Responsibilities
15261
15385
 
15262
- 1. **Fleet Monitoring** (every 30s)
15263
- - Call \`fleet_status\` + \`fleet_health\` on each heartbeat
15386
+ 1. **Fleet Monitoring** (host-assigned one-shot checks)
15387
+ - The host assigns one-shot check tasks; it does not expect routine heartbeats
15388
+ - On each assigned check, call \`fleet_status\` + \`fleet_health\`
15264
15389
  - Track what each agent is doing (task descriptions)
15265
15390
  - Detect stuck agents (>5min no events), idle agents, crashed agents
15266
15391
 
@@ -15284,31 +15409,30 @@ Your job is to observe, detect anomalies, and be ready to intervene \u2014 but o
15284
15409
  - \`hoop <agentId>\` \u2014 terminate specific agent
15285
15410
  - \`hoop all\` \u2014 terminate all running agents
15286
15411
  - \`shadow status\` \u2014 report current fleet snapshot
15287
- - \`shadow mute\` \u2014 pause heartbeat monitoring
15288
- - \`shadow resume\` \u2014 resume heartbeat monitoring
15289
- - \`shadow interval <ms>\` \u2014 change heartbeat interval
15412
+ - \`shadow mute\` \u2014 pause anomaly reporting
15413
+ - \`shadow resume\` \u2014 resume anomaly reporting
15414
+ - \`shadow interval <ms>\` \u2014 update the legacy interval setting
15290
15415
  - \`shadow model <model-id>\` \u2014 change analysis model
15291
15416
 
15292
15417
  ## Operating Rules
15293
15418
 
15294
- - **Silent by default**: Use DEBUG level logging unless anomaly detected
15419
+ - **Silent by default**: Do not send mail or status reports for healthy checks
15295
15420
  - **Deterministic**: Same state always produces same actions \u2014 no randomness
15296
- - **Report on anomaly**: When anomaly detected, use \`mail_send\` to broadcast warning
15421
+ - **Report only when needed**: Use \`mail_send\` only for high/critical anomalies or explicit control replies
15297
15422
  - **Never auto-intervene**: Always report unless explicitly commanded
15298
15423
  - **Minimal footprint**: Small state, efficient snapshots
15424
+ - **One-shot lifecycle**: Finish the assigned check and stop; do not schedule follow-up work
15299
15425
 
15300
15426
  ## Startup Sequence
15301
15427
 
15302
- 1. Send broadcast: \`shadow:started { intervalMs, model, startTime }\`
15303
- 2. Subscribe to FleetBus for all relevant events
15304
- 3. Schedule heartbeat cron job at configured interval
15305
- 4. Wait for commands or anomalies
15428
+ 1. Run one fleet snapshot with \`fleet_status\` + \`fleet_health\`
15429
+ 2. Check \`mail_inbox\` for explicit control messages
15430
+ 3. If healthy, do not send mail; final answer may be exactly \`shadow: quiet\`
15306
15431
 
15307
15432
  ## Shutdown Sequence
15308
15433
 
15309
- 1. Cancel all cron jobs (\`cron_cancel\`)
15310
- 2. Send broadcast: \`shadow:stopped { reason, finalState }\`
15311
- 3. Clean up FleetBus subscriptions`
15434
+ 1. Return only anomalies, command results, or \`shadow: quiet\`
15435
+ 2. The host stops this Shadow Agent after the assigned pass`
15312
15436
  // Budgets are set by the orchestrator per task — see fleet.ts header.
15313
15437
  };
15314
15438
  var CRITIC_AGENT = {
@@ -15363,8 +15487,13 @@ var FLEET_ROSTER_BUDGETS = {
15363
15487
  "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
15364
15488
  "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
15365
15489
  "critic": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
15366
- "shadow-agent": { timeoutMs: 24 * 60 * 60 * 1e3, maxIterations: 1e4, maxToolCalls: 5e3 },
15367
- // Long-running background monitor
15490
+ "shadow-agent": {
15491
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
15492
+ maxIterations: 2e3,
15493
+ maxToolCalls: 5e3,
15494
+ maxTokens: 6e4,
15495
+ maxCostUsd: 1
15496
+ },
15368
15497
  ...Object.fromEntries(
15369
15498
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
15370
15499
  )
@@ -15803,7 +15932,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
15803
15932
  taskIds.map((id) => {
15804
15933
  const cached = this.completedResults.find((r) => r.taskId === id);
15805
15934
  if (cached) return cached;
15806
- return new Promise((resolve7, reject) => {
15935
+ return new Promise((resolve8, reject) => {
15807
15936
  const timeout = setTimeout(() => {
15808
15937
  this.off("task.completed", handler);
15809
15938
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -15812,7 +15941,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
15812
15941
  if (result.taskId === id) {
15813
15942
  clearTimeout(timeout);
15814
15943
  this.off("task.completed", handler);
15815
- resolve7(result);
15944
+ resolve8(result);
15816
15945
  }
15817
15946
  };
15818
15947
  this.on("task.completed", handler);
@@ -16080,12 +16209,12 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
16080
16209
  }
16081
16210
  return new Promise((resolveDecision) => {
16082
16211
  let settled = false;
16083
- const resolve7 = (d) => {
16212
+ const resolve8 = (d) => {
16084
16213
  if (settled) return;
16085
16214
  settled = true;
16086
16215
  resolveDecision(d);
16087
16216
  };
16088
- const fallback = setTimeout(() => resolve7("stop"), DECISION_TIMEOUT_MS);
16217
+ const fallback = setTimeout(() => resolve8("stop"), DECISION_TIMEOUT_MS);
16089
16218
  budget._events?.emit("budget.threshold_reached", {
16090
16219
  kind: "timeout",
16091
16220
  used,
@@ -16101,11 +16230,11 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
16101
16230
  // disagreeing, resolves as a stop). Async grants still resolve.
16102
16231
  extend: (extra) => {
16103
16232
  clearTimeout(fallback);
16104
- queueMicrotask(() => resolve7({ extend: extra }));
16233
+ queueMicrotask(() => resolve8({ extend: extra }));
16105
16234
  },
16106
16235
  deny: () => {
16107
16236
  clearTimeout(fallback);
16108
- resolve7("stop");
16237
+ resolve8("stop");
16109
16238
  }
16110
16239
  });
16111
16240
  });
@@ -17015,7 +17144,7 @@ var InMemoryAgentBridge = class {
17015
17144
  });
17016
17145
  }
17017
17146
  this.inflightGuards.add(correlationId);
17018
- return new Promise((resolve7, reject) => {
17147
+ return new Promise((resolve8, reject) => {
17019
17148
  const timer = setTimeout(() => {
17020
17149
  this.inflightGuards.delete(correlationId);
17021
17150
  this.pendingRequests.delete(correlationId);
@@ -17034,7 +17163,7 @@ var InMemoryAgentBridge = class {
17034
17163
  return;
17035
17164
  }
17036
17165
  this.pendingRequests.set(correlationId, {
17037
- resolve: resolve7,
17166
+ resolve: resolve8,
17038
17167
  reject,
17039
17168
  timer
17040
17169
  });
@@ -17839,6 +17968,7 @@ function makeSpawnTool(director, roster) {
17839
17968
  usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
17840
17969
  permission: "auto",
17841
17970
  mutating: false,
17971
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
17842
17972
  inputSchema,
17843
17973
  async execute(input) {
17844
17974
  const i = input ?? {};
@@ -17917,6 +18047,7 @@ function makeAssignTool(director) {
17917
18047
  description: "Hand a task to a previously spawned subagent. Returns the task id.",
17918
18048
  permission: "auto",
17919
18049
  mutating: false,
18050
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
17920
18051
  inputSchema,
17921
18052
  async execute(input) {
17922
18053
  const i = input;
@@ -17932,6 +18063,7 @@ function makeAwaitTasksTool(director) {
17932
18063
  description: "Block until every named task completes. Returns the array of TaskResult.",
17933
18064
  permission: "auto",
17934
18065
  mutating: false,
18066
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
17935
18067
  inputSchema: { type: "object", properties: { taskIds: { type: "array", items: { type: "string" }, description: "One or more task ids returned by `assign_task`." } }, required: ["taskIds"] },
17936
18068
  async execute(input) {
17937
18069
  const i = input;
@@ -17946,6 +18078,7 @@ function makeAskTool(director) {
17946
18078
  description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
17947
18079
  permission: "auto",
17948
18080
  mutating: false,
18081
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
17949
18082
  inputSchema: {
17950
18083
  type: "object",
17951
18084
  properties: {
@@ -17981,6 +18114,7 @@ function makeAskResultTool(director) {
17981
18114
  description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
17982
18115
  permission: "auto",
17983
18116
  mutating: false,
18117
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
17984
18118
  inputSchema: {
17985
18119
  type: "object",
17986
18120
  properties: {
@@ -18008,6 +18142,7 @@ function makeRollUpTool(director) {
18008
18142
  description: "Aggregate completed task results into a single formatted summary.",
18009
18143
  permission: "auto",
18010
18144
  mutating: false,
18145
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18011
18146
  inputSchema: {
18012
18147
  type: "object",
18013
18148
  properties: {
@@ -18029,6 +18164,7 @@ function makeTerminateTool(director) {
18029
18164
  description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
18030
18165
  permission: "auto",
18031
18166
  mutating: true,
18167
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18032
18168
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
18033
18169
  async execute(input) {
18034
18170
  const i = input;
@@ -18043,6 +18179,7 @@ function makeTerminateAllTool(director) {
18043
18179
  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.',
18044
18180
  permission: "auto",
18045
18181
  mutating: true,
18182
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18046
18183
  inputSchema: { type: "object", properties: {}, required: [] },
18047
18184
  async execute() {
18048
18185
  await director.terminateAll();
@@ -18056,6 +18193,7 @@ function makeFleetStatusTool(director) {
18056
18193
  description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
18057
18194
  permission: "auto",
18058
18195
  mutating: false,
18196
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18059
18197
  inputSchema: { type: "object", properties: {}, required: [] },
18060
18198
  async execute() {
18061
18199
  const base = director.status();
@@ -18077,6 +18215,7 @@ function makeFleetUsageTool(director) {
18077
18215
  description: "Token + cost breakdown across the fleet, per-subagent and totals.",
18078
18216
  permission: "auto",
18079
18217
  mutating: false,
18218
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18080
18219
  inputSchema: { type: "object", properties: {}, required: [] },
18081
18220
  async execute() {
18082
18221
  return director.snapshot();
@@ -18089,6 +18228,7 @@ function makeFleetSessionTool(director) {
18089
18228
  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.",
18090
18229
  permission: "auto",
18091
18230
  mutating: false,
18231
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18092
18232
  inputSchema: {
18093
18233
  type: "object",
18094
18234
  properties: {
@@ -18115,6 +18255,7 @@ function makeFleetHealthTool(director) {
18115
18255
  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.",
18116
18256
  permission: "auto",
18117
18257
  mutating: false,
18258
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18118
18259
  inputSchema: { type: "object", properties: {}, required: [] },
18119
18260
  async execute() {
18120
18261
  const status = director.status();
@@ -18145,6 +18286,7 @@ function makeCollabDebugTool(director) {
18145
18286
  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).",
18146
18287
  permission: "auto",
18147
18288
  mutating: false,
18289
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18148
18290
  inputSchema: {
18149
18291
  type: "object",
18150
18292
  properties: {
@@ -18208,6 +18350,7 @@ function makeFleetEmitTool(director) {
18208
18350
  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.",
18209
18351
  permission: "auto",
18210
18352
  mutating: false,
18353
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18211
18354
  inputSchema: {
18212
18355
  type: "object",
18213
18356
  properties: {
@@ -18240,6 +18383,7 @@ function makeWorkCompleteTool(director) {
18240
18383
  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.",
18241
18384
  permission: "auto",
18242
18385
  mutating: false,
18386
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18243
18387
  inputSchema: { type: "object", properties: {}, required: [] },
18244
18388
  async execute() {
18245
18389
  director.workComplete();
@@ -18689,6 +18833,9 @@ var Director = class _Director {
18689
18833
  /** Snapshot of which subagent owns each task — drives state-checkpoint
18690
18834
  * status updates without re-walking the manifest. */
18691
18835
  taskOwners = /* @__PURE__ */ new Map();
18836
+ /** Infrastructure-owned task ids that should not appear in user-visible
18837
+ * manifest/session/checkpoint/rollup state. */
18838
+ internalTaskIds = /* @__PURE__ */ new Set();
18692
18839
  /** Cumulative auto-extension grants per subagent (all budget kinds). Lets
18693
18840
  * /fleet render "⚡ extended ×N" without replaying the event stream. */
18694
18841
  extendTotals = /* @__PURE__ */ new Map();
@@ -18802,17 +18949,21 @@ var Director = class _Director {
18802
18949
  this.fleetManager?.setCoordinator(this.coordinator);
18803
18950
  this.taskCompletedListener = (payload) => {
18804
18951
  const r = payload.result;
18805
- this.completed.set(r.taskId, r);
18806
- if (this.completed.size > _Director.MAX_COMPLETED) {
18807
- const toDelete = this.completed.size - _Director.MAX_COMPLETED;
18808
- const keys = [...this.completed.keys()].slice(0, toDelete);
18809
- for (const k of keys) this.completed.delete(k);
18952
+ const internalTask = this.internalTaskIds.delete(r.taskId);
18953
+ if (!internalTask) {
18954
+ this.completed.set(r.taskId, r);
18955
+ if (this.completed.size > _Director.MAX_COMPLETED) {
18956
+ const toDelete = this.completed.size - _Director.MAX_COMPLETED;
18957
+ const keys = [...this.completed.keys()].slice(0, toDelete);
18958
+ for (const k of keys) this.completed.delete(k);
18959
+ }
18810
18960
  }
18811
18961
  const waiter = this.taskWaiters.get(r.taskId);
18812
18962
  if (waiter) {
18813
18963
  waiter.resolve(r);
18814
18964
  this.taskWaiters.delete(r.taskId);
18815
18965
  }
18966
+ if (internalTask) return;
18816
18967
  const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
18817
18968
  const failed = r.status !== "success";
18818
18969
  const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
@@ -19482,6 +19633,23 @@ var Director = class _Director {
19482
19633
  this.scheduleManifest();
19483
19634
  return taskWithId.id;
19484
19635
  }
19636
+ /**
19637
+ * Assign infrastructure-owned work directly to the coordinator without
19638
+ * manifest/session/checkpoint bookkeeping. The task still uses the normal
19639
+ * subagent runner, budget, and completion events, but it is excluded from
19640
+ * rollups and persisted fleet task history.
19641
+ */
19642
+ async assignInternal(task) {
19643
+ const taskWithId = task.id ? task : { ...task, id: randomUUID() };
19644
+ this.internalTaskIds.add(taskWithId.id);
19645
+ try {
19646
+ await this.coordinator.assign(taskWithId);
19647
+ } catch (err) {
19648
+ this.internalTaskIds.delete(taskWithId.id);
19649
+ throw err;
19650
+ }
19651
+ return taskWithId.id;
19652
+ }
19485
19653
  /**
19486
19654
  * Block until every task id resolves. Returns results in the same
19487
19655
  * order as the input. If any task hasn't completed by the time this
@@ -19496,11 +19664,11 @@ var Director = class _Director {
19496
19664
  if (cached) return cached;
19497
19665
  const existing = this.taskWaiters.get(id);
19498
19666
  if (existing) return existing.promise;
19499
- let resolve7;
19667
+ let resolve8;
19500
19668
  const promise = new Promise((res) => {
19501
- resolve7 = res;
19669
+ resolve8 = res;
19502
19670
  });
19503
- this.taskWaiters.set(id, { promise, resolve: resolve7 });
19671
+ this.taskWaiters.set(id, { promise, resolve: resolve8 });
19504
19672
  return promise;
19505
19673
  })
19506
19674
  );
@@ -19896,7 +20064,7 @@ function createDelegateTool(opts) {
19896
20064
  subagentId
19897
20065
  });
19898
20066
  const dir = director;
19899
- const result = await new Promise((resolve7) => {
20067
+ const result = await new Promise((resolve8) => {
19900
20068
  let settled = false;
19901
20069
  let timer;
19902
20070
  const finish = (value) => {
@@ -19906,7 +20074,7 @@ function createDelegateTool(opts) {
19906
20074
  offTool();
19907
20075
  offIter();
19908
20076
  offProgress();
19909
- resolve7(value);
20077
+ resolve8(value);
19910
20078
  };
19911
20079
  const arm = () => {
19912
20080
  if (timer) clearTimeout(timer);
@@ -21238,6 +21406,14 @@ var SpecParser = class {
21238
21406
  };
21239
21407
 
21240
21408
  // src/sdd/task-generator.ts
21409
+ function extractVerificationCommand(criteria) {
21410
+ const marker = /^\s*(?:\$\s+|(?:run|verify|cmd)\s*:\s*)(.+\S)\s*$/i;
21411
+ for (const c of criteria) {
21412
+ const m = marker.exec(c);
21413
+ if (m?.[1]) return m[1].trim();
21414
+ }
21415
+ return void 0;
21416
+ }
21241
21417
  var TaskGenerator = class {
21242
21418
  constructor(opts) {
21243
21419
  this.opts = opts;
@@ -21245,15 +21421,18 @@ var TaskGenerator = class {
21245
21421
  opts;
21246
21422
  async generateFromSpec(spec) {
21247
21423
  const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
21424
+ const featureIds = [];
21248
21425
  const overview = spec.sections.find((s) => s.type === "overview");
21249
21426
  if (overview) {
21250
- this.opts.taskTracker.addNode({
21251
- title: `Implement ${spec.title}`,
21252
- description: overview.content,
21253
- type: "feature",
21254
- priority: "high",
21255
- status: "pending"
21256
- });
21427
+ featureIds.push(
21428
+ this.opts.taskTracker.addNode({
21429
+ title: `Implement ${spec.title}`,
21430
+ description: overview.content,
21431
+ type: "feature",
21432
+ priority: "high",
21433
+ status: "pending"
21434
+ }).id
21435
+ );
21257
21436
  }
21258
21437
  const byPriority = {
21259
21438
  critical: [],
@@ -21268,7 +21447,7 @@ var TaskGenerator = class {
21268
21447
  const order = ["critical", "high", "medium", "low"];
21269
21448
  for (const p of order) {
21270
21449
  for (const req of byPriority[p]) {
21271
- this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
21450
+ featureIds.push(this.opts.taskTracker.addNode(this.createTaskFromRequirement(req)).id);
21272
21451
  }
21273
21452
  }
21274
21453
  if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
@@ -21279,31 +21458,37 @@ var TaskGenerator = class {
21279
21458
  priority: "high",
21280
21459
  status: "pending"
21281
21460
  });
21461
+ featureIds.push(apiParent.id);
21282
21462
  for (const endpoint of spec.apiEndpoints) {
21283
21463
  const task = this.createTaskFromEndpoint(endpoint);
21284
- this.opts.taskTracker.addNode({
21285
- ...task,
21286
- parentId: apiParent.id
21287
- });
21464
+ featureIds.push(
21465
+ this.opts.taskTracker.addNode({
21466
+ ...task,
21467
+ parentId: apiParent.id
21468
+ }).id
21469
+ );
21288
21470
  }
21289
21471
  }
21290
- this.opts.taskTracker.addNode({
21472
+ const testId = this.opts.taskTracker.addNode({
21291
21473
  title: "Write Tests",
21292
21474
  description: "Comprehensive test coverage for all features",
21293
21475
  type: "test",
21294
21476
  priority: "high",
21295
21477
  status: "pending"
21296
- });
21297
- this.opts.taskTracker.addNode({
21478
+ }).id;
21479
+ for (const f of featureIds) this.opts.taskTracker.addDependency(f, testId);
21480
+ const docsId = this.opts.taskTracker.addNode({
21298
21481
  title: "Update Documentation",
21299
21482
  description: "Update docs for new features",
21300
21483
  type: "docs",
21301
21484
  priority: "medium",
21302
21485
  status: "pending"
21303
- });
21486
+ }).id;
21487
+ for (const f of [...featureIds, testId]) this.opts.taskTracker.addDependency(f, docsId);
21304
21488
  return graph;
21305
21489
  }
21306
21490
  createTaskFromRequirement(req) {
21491
+ const verificationCommand = this.opts.verificationFromAcceptance ? extractVerificationCommand(req.acceptanceCriteria) : void 0;
21307
21492
  return {
21308
21493
  title: req.description,
21309
21494
  description: this.buildDescription(req),
@@ -21312,7 +21497,8 @@ var TaskGenerator = class {
21312
21497
  status: "pending",
21313
21498
  specRequirementId: req.id,
21314
21499
  tags: [req.type, req.priority],
21315
- estimateHours: this.estimateHours(req)
21500
+ estimateHours: this.estimateHours(req),
21501
+ ...verificationCommand ? { metadata: { verificationCommand } } : {}
21316
21502
  };
21317
21503
  }
21318
21504
  createTaskFromEndpoint(endpoint) {
@@ -21501,6 +21687,27 @@ var TaskTracker = class {
21501
21687
  opts;
21502
21688
  graph = null;
21503
21689
  transitions = [];
21690
+ listeners = [];
21691
+ /**
21692
+ * Subscribe to live task mutations (add / update / status change). Returns an
21693
+ * unsubscribe fn. This is the hook the board projector uses to stream a live
21694
+ * snapshot — the tracker was previously fire-and-forget with no observability.
21695
+ */
21696
+ subscribe(listener) {
21697
+ this.listeners.push(listener);
21698
+ return () => {
21699
+ const i = this.listeners.indexOf(listener);
21700
+ if (i >= 0) this.listeners.splice(i, 1);
21701
+ };
21702
+ }
21703
+ notifyChange(change) {
21704
+ for (const l of this.listeners) {
21705
+ try {
21706
+ l(change);
21707
+ } catch {
21708
+ }
21709
+ }
21710
+ }
21504
21711
  /**
21505
21712
  * Attach an existing graph (used by PhaseOrchestrator to associate a tracker
21506
21713
  * with a phase's pre-built task graph without re-creating it).
@@ -21545,6 +21752,7 @@ var TaskTracker = class {
21545
21752
  }
21546
21753
  this.graph.updatedAt = now;
21547
21754
  this.persist();
21755
+ this.notifyChange({ type: "node_added", nodeId: newNode.id, node: newNode });
21548
21756
  return newNode;
21549
21757
  }
21550
21758
  addEdge(from, to, type = "depends_on") {
@@ -21561,6 +21769,68 @@ var TaskTracker = class {
21561
21769
  this.graph.updatedAt = Date.now();
21562
21770
  this.persist();
21563
21771
  }
21772
+ /**
21773
+ * Declare that `taskId` depends on `depId` (a `depends_on` edge `depId → taskId`),
21774
+ * guarding against self-loops, duplicates, missing nodes, and cycles. Returns
21775
+ * true if the dependency now holds (added or already present), false if it was
21776
+ * rejected (would create a cycle / unknown node). This is the safe entry point
21777
+ * for wiring agent-declared `dependsOn` references into the graph.
21778
+ */
21779
+ addDependency(depId, taskId) {
21780
+ if (!this.graph) return false;
21781
+ if (depId === taskId) return false;
21782
+ if (!this.graph.nodes.has(depId) || !this.graph.nodes.has(taskId)) return false;
21783
+ if (this.getBlockers(taskId).includes(depId)) return true;
21784
+ if (this.dependsOnTransitively(depId, taskId, /* @__PURE__ */ new Set())) return false;
21785
+ this.addEdge(depId, taskId, "depends_on");
21786
+ return true;
21787
+ }
21788
+ /** True when `taskId` transitively depends on `targetId` (follows depends_on blockers). */
21789
+ dependsOnTransitively(taskId, targetId, seen) {
21790
+ if (taskId === targetId) return true;
21791
+ if (seen.has(taskId)) return false;
21792
+ seen.add(taskId);
21793
+ for (const blocker of this.getBlockers(taskId)) {
21794
+ if (this.dependsOnTransitively(blocker, targetId, seen)) return true;
21795
+ }
21796
+ return false;
21797
+ }
21798
+ /**
21799
+ * Merge `patch` into a node's `metadata` (used for per-task model/provider/
21800
+ * fallback assignment and the cancel marker). Persists + notifies as a node
21801
+ * update. No-op if the node is missing.
21802
+ */
21803
+ patchMetadata(id, patch) {
21804
+ if (!this.graph) return;
21805
+ const node = this.graph.nodes.get(id);
21806
+ if (!node) return;
21807
+ node.metadata = { ...node.metadata, ...patch };
21808
+ node.updatedAt = Date.now();
21809
+ this.graph.updatedAt = node.updatedAt;
21810
+ this.persist();
21811
+ this.notifyChange({ type: "node_updated", nodeId: id, node });
21812
+ }
21813
+ /**
21814
+ * Remove a node and every edge touching it. Intended for deleting a task that
21815
+ * has not started yet — callers must gate on status (do not remove a running
21816
+ * task). Dependents simply lose this blocker (re-evaluated by `canStart`).
21817
+ * Returns true if a node was removed.
21818
+ */
21819
+ removeNode(id) {
21820
+ if (!this.graph) return false;
21821
+ const node = this.graph.nodes.get(id);
21822
+ if (!node) return false;
21823
+ this.graph.nodes.delete(id);
21824
+ this.graph.edges = this.graph.edges.filter((e) => e.from !== id && e.to !== id);
21825
+ this.graph.rootNodes = this.graph.rootNodes.filter((r) => r !== id);
21826
+ for (const n of this.graph.nodes.values()) {
21827
+ if (n.children?.includes(id)) n.children = n.children.filter((c) => c !== id);
21828
+ }
21829
+ this.graph.updatedAt = Date.now();
21830
+ this.persist();
21831
+ this.notifyChange({ type: "node_removed", nodeId: id, node });
21832
+ return true;
21833
+ }
21564
21834
  updateNodeStatus(id, status, reason) {
21565
21835
  if (!this.graph) throw new SddError({
21566
21836
  message: "No graph loaded",
@@ -21592,6 +21862,12 @@ var TaskTracker = class {
21592
21862
  }
21593
21863
  this.graph.updatedAt = now;
21594
21864
  this.persist();
21865
+ this.notifyChange({
21866
+ type: "status_changed",
21867
+ nodeId: id,
21868
+ node,
21869
+ transition: { from, to: status, timestamp: now, reason }
21870
+ });
21595
21871
  }
21596
21872
  updateNode(id, patch) {
21597
21873
  if (!this.graph) throw new SddError({
@@ -21609,9 +21885,11 @@ var TaskTracker = class {
21609
21885
  if (patch.priority !== void 0) node.priority = patch.priority;
21610
21886
  if (patch.estimateHours !== void 0) node.estimateHours = patch.estimateHours;
21611
21887
  if (patch.tags !== void 0) node.tags = patch.tags;
21888
+ if (patch.assignee !== void 0) node.assignee = patch.assignee;
21612
21889
  node.updatedAt = Date.now();
21613
21890
  this.graph.updatedAt = node.updatedAt;
21614
21891
  this.persist();
21892
+ this.notifyChange({ type: "node_updated", nodeId: id, node });
21615
21893
  }
21616
21894
  getNode(id) {
21617
21895
  return this.graph?.nodes.get(id);
@@ -21790,7 +22068,10 @@ var TaskFlow = class {
21790
22068
  throw err;
21791
22069
  }
21792
22070
  this.setPhase("generating");
21793
- const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
22071
+ const generator = new TaskGenerator({
22072
+ taskTracker: this.opts.tracker,
22073
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
22074
+ });
21794
22075
  this.graph = await generator.generateFromSpec(this.spec);
21795
22076
  return this.graph;
21796
22077
  }
@@ -22269,27 +22550,37 @@ function buildImplementationPrompt(session) {
22269
22550
  "```json",
22270
22551
  "[",
22271
22552
  " {",
22553
+ ' "id": "t1",',
22272
22554
  ' "title": "Create auth middleware",',
22273
22555
  ' "description": "Implement JWT verification middleware for protected routes",',
22274
22556
  ' "type": "feature",',
22275
22557
  ' "priority": "critical",',
22276
22558
  ' "estimateHours": 3,',
22559
+ ' "dependsOn": [],',
22277
22560
  ' "tags": ["auth", "middleware"]',
22278
22561
  " },",
22279
22562
  " {",
22563
+ ' "id": "t2",',
22280
22564
  ' "title": "Write auth tests",',
22281
22565
  ' "description": "Unit and integration tests for authentication flow",',
22282
22566
  ' "type": "test",',
22283
22567
  ' "priority": "high",',
22284
22568
  ' "estimateHours": 2,',
22569
+ ' "dependsOn": ["t1"],',
22285
22570
  ' "tags": ["test", "auth"]',
22286
22571
  " }",
22287
22572
  "]",
22288
22573
  "```",
22289
22574
  "",
22290
22575
  "Rules:",
22291
- "- Each task must be independently executable",
22292
- "- Order tasks by dependency (things that block others come first)",
22576
+ '- Give every task a short stable "id" (t1, t2, \u2026). Reference prerequisites in "dependsOn"',
22577
+ " as a list of those ids \u2014 this builds the real dependency graph that drives parallel vs",
22578
+ " sequential execution.",
22579
+ '- "dependsOn": [] means the task is independent and may run in parallel with other roots.',
22580
+ "- A task with dependsOn runs ONLY after every listed task completes. Model true ordering:",
22581
+ " tests depend on the feature they test, docs/integration depend on the parts they cover.",
22582
+ "- Do NOT create cycles (t1\u2192t2\u2192t1). Keep chains as shallow as correctness allows so",
22583
+ " independent work runs concurrently.",
22293
22584
  '- Use type: "feature" for code, "test" for tests, "docs" for documentation, "chore" for config',
22294
22585
  '- Use priority: "critical" for blockers, "high" for core features, "medium" for nice-to-haves, "low" for polish'
22295
22586
  ].join("\n");
@@ -22358,10 +22649,10 @@ var AISpecBuilder = class {
22358
22649
  async saveSession() {
22359
22650
  if (!this.sessionPath) return;
22360
22651
  try {
22361
- const fsp16 = await import('fs/promises');
22362
- const path22 = await import('path');
22652
+ const fsp18 = await import('fs/promises');
22653
+ const path23 = await import('path');
22363
22654
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
22364
- await fsp16.mkdir(path22.dirname(this.sessionPath), { recursive: true });
22655
+ await fsp18.mkdir(path23.dirname(this.sessionPath), { recursive: true });
22365
22656
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
22366
22657
  } catch {
22367
22658
  }
@@ -22370,8 +22661,8 @@ var AISpecBuilder = class {
22370
22661
  async loadSession() {
22371
22662
  if (!this.sessionPath) return false;
22372
22663
  try {
22373
- const fsp16 = await import('fs/promises');
22374
- const raw = await fsp16.readFile(this.sessionPath, "utf8");
22664
+ const fsp18 = await import('fs/promises');
22665
+ const raw = await fsp18.readFile(this.sessionPath, "utf8");
22375
22666
  const loaded = JSON.parse(raw);
22376
22667
  if (loaded?.id && loaded?.phase && loaded?.title) {
22377
22668
  this.session = loaded;
@@ -22385,8 +22676,8 @@ var AISpecBuilder = class {
22385
22676
  async deleteSession() {
22386
22677
  if (!this.sessionPath) return;
22387
22678
  try {
22388
- const fsp16 = await import('fs/promises');
22389
- await fsp16.unlink(this.sessionPath);
22679
+ const fsp18 = await import('fs/promises');
22680
+ await fsp18.unlink(this.sessionPath);
22390
22681
  } catch {
22391
22682
  }
22392
22683
  }
@@ -23088,15 +23379,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
23088
23379
  maxId = id;
23089
23380
  }
23090
23381
  }
23091
- const path22 = [];
23382
+ const path23 = [];
23092
23383
  let current = maxId;
23093
23384
  const visited = /* @__PURE__ */ new Set();
23094
23385
  while (current && !visited.has(current)) {
23095
23386
  visited.add(current);
23096
- path22.unshift(current);
23387
+ path23.unshift(current);
23097
23388
  current = prev.get(current) ?? null;
23098
23389
  }
23099
- return path22;
23390
+ return path23;
23100
23391
  }
23101
23392
  function computeParallelGroups(graph, blockedByMap) {
23102
23393
  const groups = [];
@@ -23515,6 +23806,24 @@ var SddTaskDecomposer = class {
23515
23806
  getWaveCount() {
23516
23807
  return this.wave;
23517
23808
  }
23809
+ /**
23810
+ * All ready (dependency-satisfied) pending tasks, priority-sorted — UNSLICED.
23811
+ * The continuous scheduler fills its own free slots from this list, so unlike
23812
+ * `nextBatch()` it does not cap at `slots`.
23813
+ */
23814
+ readyNodes() {
23815
+ return this.pendingReadyNodes();
23816
+ }
23817
+ /**
23818
+ * True when every node has reached a terminal state (completed or failed).
23819
+ * This — not `isDone()` (which requires ALL completed) — is the correct loop
23820
+ * exit for the continuous scheduler: a terminally-failed task must not keep
23821
+ * the run spinning to its backstop.
23822
+ */
23823
+ isSettled() {
23824
+ const nodes = this.tracker.getAllNodes();
23825
+ return nodes.length > 0 && nodes.every((n) => n.status === "completed" || n.status === "failed");
23826
+ }
23518
23827
  // -------------------------------------------------------------------
23519
23828
  // Internal helpers
23520
23829
  // -------------------------------------------------------------------
@@ -23554,65 +23863,478 @@ var SddTaskDecomposer = class {
23554
23863
  var SddParallelRun = class {
23555
23864
  constructor(opts) {
23556
23865
  this.opts = opts;
23557
- this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
23558
- this.timeoutMs = opts.taskTimeoutMs ?? 3e5;
23559
- this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
23866
+ this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 2));
23867
+ this.timeoutMs = opts.taskTimeoutMs;
23868
+ this.idleTimeoutMs = Math.max(1, opts.taskIdleTimeoutMs ?? 6e5);
23869
+ this.maxRetries = Math.max(0, opts.maxRetries ?? 3);
23870
+ this.maxSupervisorEscalations = Math.max(0, opts.maxSupervisorEscalations ?? 2);
23871
+ this.maxFailedSweeps = Math.max(0, opts.maxFailedRetrySweeps ?? 2);
23872
+ this.runId = opts.runId ?? `sdd-${randomUUID().slice(0, 8)}`;
23873
+ this.events = opts.events;
23874
+ this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
23875
+ this.maxWallClockMs = opts.maxWallClockMs;
23876
+ this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
23560
23877
  this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, { parallelSlots: this.slots });
23561
23878
  }
23562
23879
  opts;
23563
23880
  slots;
23881
+ /** Opt-in hard wall-clock cap (undefined → no cap; idle reaper guards instead). */
23564
23882
  timeoutMs;
23883
+ /** Idle reaper window (ms) — resets on activity; reaps only a genuine stall. */
23884
+ idleTimeoutMs;
23565
23885
  maxRetries;
23886
+ /** Max supervisor rescues per task before it must terminal-fail (loop guard). */
23887
+ maxSupervisorEscalations;
23888
+ /** Per-task count of supervisor rescues used (resets nothing — bounds the loop). */
23889
+ supervisorEscalations = /* @__PURE__ */ new Map();
23890
+ /** Max end-of-run failed-task sweeps (see `maxFailedRetrySweeps`). */
23891
+ maxFailedSweeps;
23892
+ /** How many failed-task sweeps have run this `run()` so far. */
23893
+ failedSweeps = 0;
23894
+ /** Completed-count snapshot at the last sweep, to detect a no-progress sweep. */
23895
+ lastSweepCompleted = 0;
23566
23896
  decomposer;
23567
23897
  coordinator = null;
23568
23898
  stopRequested = false;
23569
23899
  retryMap = /* @__PURE__ */ new Map();
23900
+ runId;
23901
+ events;
23902
+ maxTotalWaves;
23903
+ maxWallClockMs;
23904
+ maxRecoveryRounds;
23905
+ recoveryRounds = 0;
23906
+ /** Per-run worker identities, so the board shows "who is on what". */
23907
+ usedNicknames = /* @__PURE__ */ new Set();
23908
+ /** Per-task git worktree cwd (Layer 2 worktree isolation; empty otherwise). */
23909
+ taskCwds = /* @__PURE__ */ new Map();
23910
+ /** Per-task git worktree branch, for board display. */
23911
+ taskBranches = /* @__PURE__ */ new Map();
23912
+ /** Live worktree handles keyed by task id (for commit/merge/release). */
23913
+ taskWorktrees = /* @__PURE__ */ new Map();
23914
+ /** Live subagent id per running task — lets cancelTask() abort exactly one. */
23915
+ taskSubagents = /* @__PURE__ */ new Map();
23916
+ /** Tasks the user cancelled mid-flight — skip retry, mark terminal-cancelled. */
23917
+ cancelledTasks = /* @__PURE__ */ new Set();
23918
+ /**
23919
+ * Base branch the run's squash commits land on (captured once at start when
23920
+ * worktrees are enabled). Anchors a later `rollback()`.
23921
+ */
23922
+ baseBranch;
23923
+ /**
23924
+ * Squash-merge commits this run landed on the base branch, in landing order.
23925
+ * `rollback()` reverts these (newest → oldest). Persisted via the board
23926
+ * snapshot so a post-run rollback can read them off disk.
23927
+ */
23928
+ mergedCommits = [];
23929
+ /** Monotonic dispatch counter (unique subagent ids) + dispatch-round counter. */
23930
+ dispatchSeq = 0;
23931
+ round = 0;
23932
+ /** Type-safe emit on the optional EventBus (no-op when unwired). */
23933
+ emit(event, payload) {
23934
+ this.events?.emit(event, payload);
23935
+ }
23570
23936
  // -------------------------------------------------------------------
23571
23937
  // Public API
23572
23938
  // -------------------------------------------------------------------
23939
+ paused = false;
23573
23940
  /** Trigger stop — causes run() to abort after the current wave. */
23574
23941
  stop() {
23575
23942
  this.stopRequested = true;
23943
+ this.paused = false;
23576
23944
  this.coordinator?.stopAll();
23577
23945
  }
23578
- /** Execute all waves until completion or deadlock. Returns final summary. */
23946
+ /** Pause: no new wave starts until resume() (the current wave finishes). */
23947
+ pause() {
23948
+ this.paused = true;
23949
+ }
23950
+ resume() {
23951
+ this.paused = false;
23952
+ }
23953
+ isPaused() {
23954
+ return this.paused;
23955
+ }
23956
+ isRunning() {
23957
+ return !this.stopRequested && !this.decomposer.isSettled();
23958
+ }
23959
+ /** Base branch the run's squash commits land on (undefined when worktrees off). */
23960
+ getBaseBranch() {
23961
+ return this.baseBranch;
23962
+ }
23963
+ /** Squash commits this run landed on the base branch, in landing order. */
23964
+ getMergedCommits() {
23965
+ return this.mergedCommits;
23966
+ }
23967
+ /**
23968
+ * Remove every git worktree + branch this run (and any prior run) created.
23969
+ * Refuses while the run is still live — cleaning a checkout under an active
23970
+ * worker would corrupt it. Stop first. Returns the number of worktrees removed
23971
+ * (0 when worktrees are disabled). Idempotent.
23972
+ */
23973
+ async cleanupWorktrees() {
23974
+ if (this.isRunning()) return 0;
23975
+ const wt = this.opts.worktrees;
23976
+ if (!wt) return 0;
23977
+ for (const [taskId, handle] of [...this.taskWorktrees]) {
23978
+ await wt.release(handle, { keep: false }).catch(() => {
23979
+ });
23980
+ this.forgetWorktree(taskId);
23981
+ }
23982
+ const { removed } = await wt.cleanupAllManaged();
23983
+ return removed;
23984
+ }
23985
+ /**
23986
+ * Undo the run's merged commits by reverting each on the base branch (history
23987
+ * preserving). Refuses while the run is still live (stop first). Returns the
23988
+ * revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
23989
+ */
23990
+ async rollback() {
23991
+ if (this.isRunning()) return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
23992
+ const wt = this.opts.worktrees;
23993
+ if (!wt || !this.baseBranch) {
23994
+ return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
23995
+ }
23996
+ return wt.revertCommits(
23997
+ this.baseBranch,
23998
+ this.mergedCommits.map((c) => c.sha)
23999
+ );
24000
+ }
24001
+ /** Requeue a task to `pending` so the scheduler re-runs it (clears retries + cancel marker). */
24002
+ retryTask(taskId) {
24003
+ if (!this.opts.tracker.getNode(taskId)) return false;
24004
+ this.retryMap.delete(taskId);
24005
+ this.persistRetries(taskId, 0);
24006
+ this.cancelledTasks.delete(taskId);
24007
+ this.opts.tracker.patchMetadata(taskId, { cancelled: void 0 });
24008
+ this.opts.tracker.updateNodeStatus(taskId, "pending", "manual retry");
24009
+ return true;
24010
+ }
24011
+ /** Reassign a task to a specific agent name (reflected on the board). */
24012
+ reassignTask(taskId, agentName) {
24013
+ if (!this.opts.tracker.getNode(taskId)) return false;
24014
+ this.opts.tracker.updateNode(taskId, { assignee: agentName });
24015
+ return true;
24016
+ }
24017
+ /**
24018
+ * Set/override a task's worker model (and optionally provider) — applied on its
24019
+ * NEXT dispatch (a running task must be cancelled + retried to take effect). The
24020
+ * assignment lives on node metadata so it survives crash → resume.
24021
+ */
24022
+ setTaskModel(taskId, model, provider) {
24023
+ if (!this.opts.tracker.getNode(taskId)) return false;
24024
+ this.opts.tracker.patchMetadata(taskId, { model, ...provider !== void 0 ? { provider } : {} });
24025
+ return true;
24026
+ }
24027
+ /** Set/override a task's fallback model chain (applied on its next dispatch). */
24028
+ setTaskFallbacks(taskId, fallbackModels) {
24029
+ if (!this.opts.tracker.getNode(taskId)) return false;
24030
+ this.opts.tracker.patchMetadata(taskId, { fallbackModels });
24031
+ return true;
24032
+ }
24033
+ /**
24034
+ * Set/override a task's verification command (the completion gate runs it in
24035
+ * the task's cwd and only lets the task complete on exit 0). Empty/undefined
24036
+ * clears it. Applied on the task's next verification — i.e. its next dispatch.
24037
+ */
24038
+ setTaskVerification(taskId, verificationCommand) {
24039
+ if (!this.opts.tracker.getNode(taskId)) return false;
24040
+ const cmd = verificationCommand?.trim();
24041
+ this.opts.tracker.patchMetadata(taskId, { verificationCommand: cmd ? cmd : void 0 });
24042
+ return true;
24043
+ }
24044
+ /**
24045
+ * Cancel a task. If it is currently running, abort its subagent and mark the
24046
+ * node terminally failed+cancelled (so the scheduler frees the slot and does
24047
+ * NOT retry it). If it has not started, it is simply marked cancelled. Use
24048
+ * `retryTask` to bring a cancelled task back. Returns false for an unknown task.
24049
+ */
24050
+ async cancelTask(taskId) {
24051
+ const node = this.opts.tracker.getNode(taskId);
24052
+ if (!node) return false;
24053
+ this.cancelledTasks.add(taskId);
24054
+ this.opts.tracker.patchMetadata(taskId, { cancelled: true });
24055
+ this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
24056
+ this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId: "", error: "cancelled by user" });
24057
+ const subagentId = this.taskSubagents.get(taskId);
24058
+ if (subagentId && this.coordinator) {
24059
+ await this.coordinator.stop(subagentId).catch(() => {
24060
+ });
24061
+ }
24062
+ return true;
24063
+ }
24064
+ /**
24065
+ * Delete a not-yet-started task from the graph (pending/blocked/failed only —
24066
+ * never a running task; cancel it first). Removes the node and every edge
24067
+ * touching it; dependents lose this blocker. Returns false if missing or running.
24068
+ */
24069
+ deleteTask(taskId) {
24070
+ const node = this.opts.tracker.getNode(taskId);
24071
+ if (!node) return false;
24072
+ if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return false;
24073
+ this.cancelledTasks.delete(taskId);
24074
+ this.retryMap.delete(taskId);
24075
+ return this.opts.tracker.removeNode(taskId);
24076
+ }
24077
+ /**
24078
+ * Split a task into sub-tasks and delegate them to separate workers. The new
24079
+ * leaves inherit the parent's blockers (so they don't start before the
24080
+ * parent's dependencies are met), every existing dependent is rewired to
24081
+ * depend on ALL leaves (so downstream work waits for the whole split), and the
24082
+ * parent becomes a `completed` container. Refuses a running task (cancel it
24083
+ * first) or empty subtask list. Returns the new leaf ids (empty on refusal).
24084
+ * The scheduler picks the new pending leaves up on its next dispatch pass.
24085
+ */
24086
+ splitTask(taskId, subtasks) {
24087
+ const tracker = this.opts.tracker;
24088
+ const node = tracker.getNode(taskId);
24089
+ if (!node) return [];
24090
+ if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return [];
24091
+ if (!subtasks.length) return [];
24092
+ const blockers = tracker.getBlockers(taskId);
24093
+ const dependents = tracker.getDependents(taskId);
24094
+ const leafIds = subtasks.map(
24095
+ (s) => tracker.addNode({
24096
+ title: s.title,
24097
+ description: s.description,
24098
+ type: s.type ?? node.type,
24099
+ priority: s.priority ?? node.priority,
24100
+ status: "pending",
24101
+ parentId: taskId
24102
+ }).id
24103
+ );
24104
+ for (const leaf of leafIds) {
24105
+ for (const b of blockers) tracker.addDependency(b, leaf);
24106
+ for (const dep of dependents) tracker.addDependency(leaf, dep);
24107
+ }
24108
+ this.retryMap.delete(taskId);
24109
+ this.persistRetries(taskId, 0);
24110
+ tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
24111
+ this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
24112
+ return leafIds;
24113
+ }
24114
+ async waitWhilePaused() {
24115
+ while (this.paused && !this.stopRequested) {
24116
+ await new Promise((r) => setTimeout(r, 100));
24117
+ }
24118
+ }
24119
+ /**
24120
+ * Continuous dependency-driven execution. Unlike a wave-barrier loop (where a
24121
+ * whole batch must finish before the next starts), this fills free worker
24122
+ * slots the instant a task's dependencies are satisfied: a fast task's
24123
+ * dependent starts immediately rather than waiting for a slow sibling. Truly
24124
+ * independent tasks run in parallel; dependency chains run in order. Returns
24125
+ * the final summary when the graph settles, deadlocks, stops, or hits a backstop.
24126
+ */
23579
24127
  async run() {
23580
24128
  this.stopRequested = false;
23581
- this.retryMap.clear();
24129
+ this.restoreRetryMap();
23582
24130
  const startTime = Date.now();
23583
- let totalCompleted = 0;
23584
- let totalFailed = 0;
23585
- let totalWaves = 0;
24131
+ this.round = 0;
24132
+ this.dispatchSeq = 0;
24133
+ let totalDispatched = 0;
23586
24134
  this.buildCoordinator();
23587
- while (!this.stopRequested && !this.decomposer.isDone()) {
23588
- const batch = this.decomposer.nextBatch();
23589
- if (batch.deadlocked) {
23590
- break;
24135
+ if (this.opts.worktrees && !this.baseBranch) {
24136
+ const base = await this.opts.worktrees.currentBase().catch(() => null);
24137
+ if (base) this.baseBranch = base.branch;
24138
+ }
24139
+ this.emit("sdd.run.started", {
24140
+ runId: this.runId,
24141
+ graphId: this.opts.graph.id,
24142
+ specId: this.opts.graph.specId,
24143
+ total: this.opts.graph.nodes.size,
24144
+ baseBranch: this.baseBranch
24145
+ });
24146
+ this.recoveryRounds = 0;
24147
+ this.failedSweeps = 0;
24148
+ this.lastSweepCompleted = 0;
24149
+ let deadlocked = false;
24150
+ const running = /* @__PURE__ */ new Map();
24151
+ const dispatch = (task) => {
24152
+ totalDispatched++;
24153
+ const tracked = (async () => {
24154
+ try {
24155
+ return await this.executeOne(task);
24156
+ } catch (err) {
24157
+ this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
24158
+ this.emit("sdd.task.failed", { runId: this.runId, taskId: task.id, subagentId: "", error: String(err) });
24159
+ return { taskId: task.id, success: false };
24160
+ } finally {
24161
+ running.delete(task.id);
24162
+ }
24163
+ })();
24164
+ running.set(task.id, tracked);
24165
+ };
24166
+ while (!this.stopRequested) {
24167
+ if (totalDispatched >= this.maxTotalWaves) break;
24168
+ if (this.maxWallClockMs && Date.now() - startTime >= this.maxWallClockMs) break;
24169
+ await this.waitWhilePaused();
24170
+ if (this.stopRequested) break;
24171
+ let dispatchedThisRound = 0;
24172
+ if (running.size < this.slots) {
24173
+ const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
24174
+ for (const task of ready) {
24175
+ if (running.size >= this.slots) break;
24176
+ dispatch(task);
24177
+ dispatchedThisRound++;
24178
+ }
23591
24179
  }
23592
- if (batch.tasks.length === 0 && batch.allDone) {
24180
+ if (dispatchedThisRound > 0) {
24181
+ this.emit("sdd.wave", { runId: this.runId, wave: this.round, batchSize: dispatchedThisRound });
24182
+ this.round++;
24183
+ }
24184
+ if (running.size === 0) {
24185
+ if (this.decomposer.isSettled()) {
24186
+ const completed = this.opts.tracker.getProgress().completed;
24187
+ const madeProgress = this.failedSweeps === 0 || completed > this.lastSweepCompleted;
24188
+ if (this.failedSweeps < this.maxFailedSweeps && madeProgress && this.requeueFailedTasks() > 0) {
24189
+ this.lastSweepCompleted = completed;
24190
+ this.failedSweeps++;
24191
+ continue;
24192
+ }
24193
+ break;
24194
+ }
24195
+ const chains = this.computeDeadlockChains();
24196
+ if (chains.length > 0) {
24197
+ this.emit("sdd.deadlock", { runId: this.runId, chains });
24198
+ if (this.recoveryRounds < this.maxRecoveryRounds && this.recoverFailedBlockers()) {
24199
+ this.recoveryRounds++;
24200
+ continue;
24201
+ }
24202
+ deadlocked = true;
24203
+ }
23593
24204
  break;
23594
24205
  }
23595
- const waveResult = await this.executeWave(batch);
23596
- totalWaves++;
23597
- totalCompleted += waveResult.successCount;
23598
- totalFailed += waveResult.failCount;
23599
- this.decomposer.acknowledgeBatch(batch.tasks.map((t) => t.id));
23600
- this.opts.onWave?.(waveResult);
23601
- const progress = this.buildProgress();
23602
- this.opts.onProgress?.(progress);
23603
- if (this.stopRequested) break;
24206
+ const moreReadyNow = running.size < this.slots && this.decomposer.readyNodes().some((t) => !running.has(t.id));
24207
+ if (!moreReadyNow) {
24208
+ await Promise.race(running.values());
24209
+ this.opts.onProgress?.(this.buildProgress());
24210
+ }
23604
24211
  }
24212
+ if (running.size > 0) await Promise.allSettled(running.values());
24213
+ if (this.stopRequested) await this.teardown();
23605
24214
  const finalProgress = this.opts.tracker.getProgress();
24215
+ this.emit("sdd.run.finished", {
24216
+ runId: this.runId,
24217
+ deadlocked,
24218
+ completed: finalProgress.completed,
24219
+ failed: finalProgress.failed,
24220
+ stopped: this.stopRequested
24221
+ });
23606
24222
  return {
23607
- totalWaves,
23608
- totalCompleted,
23609
- totalFailed,
24223
+ totalWaves: this.round,
24224
+ totalCompleted: finalProgress.completed,
24225
+ totalFailed: finalProgress.failed,
23610
24226
  totalDurationMs: Date.now() - startTime,
23611
- deadlocked: !this.decomposer.isDone() && this.stopRequested === false,
24227
+ deadlocked,
23612
24228
  stopRequested: this.stopRequested,
23613
24229
  finalProgress
23614
24230
  };
23615
24231
  }
24232
+ /**
24233
+ * Compute the blocking chains for a deadlock: every still-incomplete task and
24234
+ * the blockers (by node id) that are NOT completed. Failed blockers are
24235
+ * included since they're the usual deadlock cause once retries are exhausted.
24236
+ */
24237
+ computeDeadlockChains() {
24238
+ const tracker = this.opts.tracker;
24239
+ const chains = [];
24240
+ for (const node of tracker.getAllNodes()) {
24241
+ if (node.status === "completed" || node.status === "failed") continue;
24242
+ const blockedBy = tracker.getBlockers(node.id).filter((id) => tracker.getNode(id)?.status !== "completed");
24243
+ if (blockedBy.length > 0) chains.push({ blocked: node.id, blockedBy });
24244
+ }
24245
+ return chains;
24246
+ }
24247
+ /** Requeue failed tasks that block an incomplete dependent. Returns true if any. */
24248
+ recoverFailedBlockers() {
24249
+ const tracker = this.opts.tracker;
24250
+ let recovered = false;
24251
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
24252
+ const blocksIncomplete = tracker.getDependents(node.id).some((d) => {
24253
+ const s = tracker.getNode(d)?.status;
24254
+ return s !== "completed" && s !== "failed";
24255
+ });
24256
+ if (blocksIncomplete) {
24257
+ this.retryMap.delete(node.id);
24258
+ this.persistRetries(node.id, 0);
24259
+ tracker.updateNodeStatus(node.id, "pending", "deadlock recovery");
24260
+ recovered = true;
24261
+ }
24262
+ }
24263
+ return recovered;
24264
+ }
24265
+ /**
24266
+ * Requeue every terminal-failed task that the user did NOT cancel, giving each
24267
+ * a fresh `maxRetries` budget. Shared by the automatic end-of-run sweep and
24268
+ * the manual "retry all failed" control. Returns the number requeued.
24269
+ */
24270
+ requeueFailedTasks(reason = "retry failed sweep") {
24271
+ const tracker = this.opts.tracker;
24272
+ let n = 0;
24273
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
24274
+ if (this.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
24275
+ this.retryMap.delete(node.id);
24276
+ this.persistRetries(node.id, 0);
24277
+ tracker.updateNodeStatus(node.id, "pending", reason);
24278
+ this.emit("sdd.task.retrying", {
24279
+ runId: this.runId,
24280
+ taskId: node.id,
24281
+ attempt: 0,
24282
+ maxRetries: this.maxRetries
24283
+ });
24284
+ n++;
24285
+ }
24286
+ return n;
24287
+ }
24288
+ /**
24289
+ * Manually requeue all failed tasks to `pending` (board "Retry all failed").
24290
+ * Unlike the automatic sweep this also clears any `cancelled` marker, so a
24291
+ * user can bring cancelled tasks back in the same action — mirroring
24292
+ * `retryTask`. Picked up by the running scheduler on its next dispatch pass.
24293
+ * Returns the number of tasks requeued.
24294
+ */
24295
+ retryAllFailed() {
24296
+ const failed = this.opts.tracker.getAllNodes({ status: ["failed"] });
24297
+ for (const node of failed) {
24298
+ this.cancelledTasks.delete(node.id);
24299
+ this.opts.tracker.patchMetadata(node.id, { cancelled: void 0 });
24300
+ }
24301
+ return this.requeueFailedTasks("manual retry all");
24302
+ }
24303
+ /** Restore per-task retry counts persisted in node metadata (resume support). */
24304
+ restoreRetryMap() {
24305
+ this.retryMap.clear();
24306
+ for (const node of this.opts.tracker.getAllNodes()) {
24307
+ const r = node.metadata?.retries;
24308
+ if (typeof r === "number" && r > 0) this.retryMap.set(node.id, r);
24309
+ }
24310
+ }
24311
+ /**
24312
+ * Reset orphaned `in_progress` tasks (no agent runs them after a crash) back
24313
+ * to `pending` so a fresh run re-executes them. Call before constructing a run
24314
+ * from a reloaded graph. Static so callers don't need a run instance.
24315
+ */
24316
+ static resetOrphans(tracker) {
24317
+ let n = 0;
24318
+ for (const node of tracker.getAllNodes({ status: ["in_progress"] })) {
24319
+ tracker.updateNodeStatus(node.id, "pending", "resume: orphaned in_progress");
24320
+ n++;
24321
+ }
24322
+ return n;
24323
+ }
24324
+ /** Clean teardown after a stop: reset interrupted tasks + release worktrees. */
24325
+ async teardown() {
24326
+ for (const node of this.opts.tracker.getAllNodes({ status: ["in_progress"] })) {
24327
+ this.opts.tracker.updateNodeStatus(node.id, "pending", "run stopped");
24328
+ }
24329
+ const wt = this.opts.worktrees;
24330
+ if (wt) {
24331
+ for (const [taskId, handle] of [...this.taskWorktrees]) {
24332
+ await wt.release(handle, { keep: true }).catch(() => {
24333
+ });
24334
+ this.forgetWorktree(taskId);
24335
+ }
24336
+ }
24337
+ }
23616
24338
  // -------------------------------------------------------------------
23617
24339
  // Internal
23618
24340
  // -------------------------------------------------------------------
@@ -23620,7 +24342,14 @@ var SddParallelRun = class {
23620
24342
  const config = {
23621
24343
  coordinatorId: `sdd-parallel-${randomUUID().slice(0, 8)}`,
23622
24344
  maxConcurrent: this.slots,
23623
- doneCondition: { type: "all_tasks_done" }
24345
+ doneCondition: { type: "all_tasks_done" },
24346
+ // Default budget guard for every spawned worker: idle reaper (resets on
24347
+ // activity) plus the opt-in wall-clock cap when one was configured. This
24348
+ // ensures the reaper applies even if a per-spawn config path is bypassed.
24349
+ defaultBudget: {
24350
+ idleTimeoutMs: this.idleTimeoutMs,
24351
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
24352
+ }
23624
24353
  };
23625
24354
  this.coordinator = new DefaultMultiAgentCoordinator(config);
23626
24355
  const baseFactory = this.opts.subagentFactory ?? this.defaultFactory();
@@ -23634,22 +24363,89 @@ var SddParallelRun = class {
23634
24363
  events: this.opts.agent.events
23635
24364
  });
23636
24365
  }
24366
+ /**
24367
+ * Execute a batch of tasks together. Retained as a thin wrapper over the
24368
+ * single-task primitive `executeOne` so the wave-oriented tests and any
24369
+ * batch callers keep working; the continuous scheduler in `run()` calls
24370
+ * `executeOne` directly. Throws if no coordinator is wired or a spawn fails
24371
+ * (surfaced from `executeOne`), preserving the original all-or-nothing contract.
24372
+ */
23637
24373
  async executeWave(batch) {
23638
- const wave = batch.wave;
23639
- const tasks = batch.tasks;
23640
24374
  const waveStart = Date.now();
23641
- for (const task of tasks) {
23642
- this.opts.tracker.updateNodeStatus(task.id, "in_progress");
24375
+ const outcomes = await Promise.all(batch.tasks.map((task) => this.executeOne(task)));
24376
+ const results = outcomes.map((o) => o.result).filter((r) => Boolean(r));
24377
+ const successCount = outcomes.filter((o) => o.success).length;
24378
+ const failCount = outcomes.length - successCount;
24379
+ return {
24380
+ wave: batch.wave,
24381
+ batch,
24382
+ results,
24383
+ successCount,
24384
+ failCount,
24385
+ durationMs: Date.now() - waveStart,
24386
+ stopRequested: this.stopRequested
24387
+ };
24388
+ }
24389
+ /**
24390
+ * Execute one task end-to-end: assign a worker identity, allocate its worktree,
24391
+ * spawn + assign the subagent, await its result, then update tracker status
24392
+ * (success / retry / terminal-fail / cancelled) and resolve the worktree. This
24393
+ * is the unit the continuous scheduler dispatches into a free slot. Throws on a
24394
+ * missing coordinator or failed spawn so callers can enforce all-or-nothing.
24395
+ */
24396
+ async executeOne(task) {
24397
+ const taskId = task.id;
24398
+ let agentName = task.assignee;
24399
+ if (!agentName) {
24400
+ const nick = assignNickname("executor", this.usedNicknames);
24401
+ this.usedNicknames.add(nick.key);
24402
+ agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
24403
+ this.opts.tracker.updateNode(taskId, { assignee: agentName });
24404
+ }
24405
+ this.opts.tracker.updateNodeStatus(taskId, "in_progress");
24406
+ await this.allocateWorktrees([task]);
24407
+ if (!this.coordinator) throw new SddError({
24408
+ message: "SDD parallel runner requires a coordinator",
24409
+ code: ERROR_CODES.SDD_INVALID_STATE
24410
+ });
24411
+ const coordinator = this.coordinator;
24412
+ const subagentId = `sdd-d${this.dispatchSeq++}`;
24413
+ const correlationId = randomUUID();
24414
+ const meta = task.metadata ?? {};
24415
+ const model = (typeof meta.model === "string" ? meta.model : void 0) ?? this.opts.defaultModel;
24416
+ const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? this.opts.defaultProvider;
24417
+ const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
24418
+ const spawnResult = await coordinator.spawn({
24419
+ id: subagentId,
24420
+ name: agentName ?? subagentId,
24421
+ role: "executor",
24422
+ // Idle reaper is always on; the hard wall-clock cap only when opted in.
24423
+ idleTimeoutMs: this.idleTimeoutMs,
24424
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
24425
+ cwd: this.taskCwds.get(taskId),
24426
+ disabledTools: ["delegate"],
24427
+ ...model ? { model } : {},
24428
+ ...provider ? { provider } : {},
24429
+ ...fallbackModels && fallbackModels.length ? { fallbackModels } : {}
24430
+ });
24431
+ if (!spawnResult.subagentId) {
24432
+ throw new SddError({
24433
+ message: "One or more subagent spawns failed",
24434
+ code: ERROR_CODES.SDD_INVALID_STATE
24435
+ });
23643
24436
  }
23644
- const progress = computeTaskProgress(this.opts.graph);
23645
- const taskIds = tasks.map(() => randomUUID());
23646
- const subagentIds = tasks.map((_, i) => `sdd-wave${wave}-${i}`);
24437
+ this.taskSubagents.set(taskId, subagentId);
24438
+ this.emit("sdd.task.started", {
24439
+ runId: this.runId,
24440
+ taskId,
24441
+ subagentId,
24442
+ agentName: agentName ?? "",
24443
+ worktreeBranch: this.taskBranches.get(taskId)
24444
+ });
23647
24445
  const directivePreamble = [
23648
24446
  "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
23649
24447
  "",
23650
- `Wave ${wave + 1} of ~${Math.ceil(progress.total / this.slots)}`,
23651
24448
  `Graph: ${this.opts.graph.title}`,
23652
- `Parallel slots: ${tasks.length}`,
23653
24449
  "",
23654
24450
  "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
23655
24451
  "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
@@ -23657,91 +24453,297 @@ var SddParallelRun = class {
23657
24453
  "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
23658
24454
  "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
23659
24455
  ].join("\n");
23660
- if (!this.coordinator) throw new SddError({
23661
- message: "SDD parallel runner requires a coordinator",
23662
- code: ERROR_CODES.SDD_INVALID_STATE
23663
- });
23664
- const coordinator = this.coordinator;
23665
- const spawns = subagentIds.map(
23666
- (subagentId) => coordinator.spawn({
23667
- id: subagentId,
23668
- name: subagentId,
23669
- role: "executor",
23670
- timeoutMs: this.timeoutMs,
23671
- // Disable delegation subagents are leaf workers, not orchestrators
23672
- disabledTools: ["delegate"]
23673
- })
23674
- );
23675
- const spawnResults = await Promise.all(spawns);
23676
- if (!spawnResults.every((r) => Boolean(r.subagentId))) {
23677
- throw new SddError({
23678
- message: "One or more subagent spawns failed",
23679
- code: ERROR_CODES.SDD_INVALID_STATE
23680
- });
23681
- }
23682
- const assignPromises = tasks.map((task, i) => {
23683
- const spec = {
23684
- id: taskIds[i] ?? task.id,
23685
- description: [
23686
- directivePreamble,
23687
- "",
23688
- `\u2500\u2500 TASK ${i + 1}/${tasks.length} \u2500\u2500`,
23689
- `[${task.priority.toUpperCase()}] ${task.title}`,
23690
- "",
23691
- task.description
23692
- ].join("\n"),
23693
- subagentId: subagentIds[i] ?? spawnResults[i]?.subagentId ?? task.id,
23694
- timeoutMs: this.timeoutMs
23695
- };
23696
- return this.coordinator?.assign(spec);
24456
+ await coordinator.assign({
24457
+ id: correlationId,
24458
+ description: [
24459
+ directivePreamble,
24460
+ "",
24461
+ `\u2500\u2500 TASK \u2500\u2500`,
24462
+ `[${task.priority.toUpperCase()}] ${task.title}`,
24463
+ "",
24464
+ task.description
24465
+ ].join("\n"),
24466
+ subagentId,
24467
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
23697
24468
  });
23698
- await Promise.all(assignPromises);
23699
- let results;
24469
+ let result;
23700
24470
  try {
23701
- results = await coordinator.awaitTasks(taskIds);
24471
+ const got = await coordinator.awaitTasks([correlationId]);
24472
+ result = expectDefined(got[0]);
23702
24473
  } catch (err) {
23703
- results = taskIds.map((id) => ({
23704
- subagentId: "",
23705
- taskId: id,
24474
+ result = {
24475
+ subagentId,
24476
+ taskId: correlationId,
23706
24477
  status: "failed",
23707
24478
  error: { kind: "unknown", message: String(err), retryable: false },
23708
24479
  iterations: 0,
23709
24480
  toolCalls: 0,
23710
24481
  durationMs: 0
23711
- }));
24482
+ };
23712
24483
  }
23713
- const successCount = results.filter((r) => r.status === "success").length;
23714
- const failCount = results.length - successCount;
23715
- for (let i = 0; i < results.length; i++) {
23716
- const result = expectDefined(results[i]);
23717
- const taskId = expectDefined(tasks[i]).id;
23718
- if (result.status === "success") {
23719
- this.opts.tracker.updateNodeStatus(taskId, "completed");
24484
+ this.taskSubagents.delete(taskId);
24485
+ if (this.cancelledTasks.has(taskId)) {
24486
+ await this.resolveWorktrees([task]);
24487
+ return { taskId, success: false, result };
24488
+ }
24489
+ let verificationFailReason;
24490
+ if (result.status === "success" && this.opts.verifyTask) {
24491
+ const cwd = this.taskCwds.get(taskId) ?? this.opts.projectRoot;
24492
+ try {
24493
+ const verdict = await this.opts.verifyTask({ task, result, cwd });
24494
+ if (!verdict.ok) {
24495
+ verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
24496
+ }
24497
+ } catch (err) {
24498
+ verificationFailReason = `verification error: ${String(err)}`;
24499
+ }
24500
+ if (verificationFailReason) {
24501
+ this.emit("sdd.task.verification_failed", {
24502
+ runId: this.runId,
24503
+ taskId,
24504
+ reason: verificationFailReason
24505
+ });
24506
+ }
24507
+ }
24508
+ let success = false;
24509
+ if (result.status === "success" && !verificationFailReason) {
24510
+ const merged = await this.integrateWorktree(task, result);
24511
+ if (merged.ok) {
24512
+ success = true;
24513
+ this.opts.tracker.updateNodeStatus(taskId, "completed");
23720
24514
  this.retryMap.delete(taskId);
24515
+ this.persistRetries(taskId, 0);
24516
+ this.emit("sdd.task.completed", {
24517
+ runId: this.runId,
24518
+ taskId,
24519
+ subagentId,
24520
+ durationMs: result.durationMs
24521
+ });
24522
+ } else if (merged.reason) {
24523
+ this.emit("sdd.task.verification_failed", {
24524
+ runId: this.runId,
24525
+ taskId,
24526
+ reason: merged.reason
24527
+ });
24528
+ await this.applyTaskFailure(taskId, subagentId, merged.reason);
23721
24529
  } else {
23722
- const errMsg = result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error";
23723
- const currentRetries = this.retryMap.get(taskId) ?? 0;
23724
- if (currentRetries < this.maxRetries) {
23725
- this.retryMap.set(taskId, currentRetries + 1);
23726
- this.opts.tracker.updateNodeStatus(
23727
- taskId,
23728
- "pending",
23729
- `Retry ${currentRetries + 1}/${this.maxRetries}: ${errMsg}`
23730
- );
24530
+ this.emit("sdd.task.conflict", {
24531
+ runId: this.runId,
24532
+ taskId,
24533
+ conflictFiles: merged.conflictFiles ?? []
24534
+ });
24535
+ const reason = `merge conflict${merged.conflictFiles?.length ? `: ${merged.conflictFiles.join(", ")}` : ""}`;
24536
+ await this.applyTaskFailure(taskId, subagentId, reason);
24537
+ }
24538
+ } else {
24539
+ const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
24540
+ await this.applyTaskFailure(taskId, subagentId, errMsg);
24541
+ await this.resolveWorktrees([task]);
24542
+ }
24543
+ return { taskId, success, result };
24544
+ }
24545
+ /**
24546
+ * Apply a task failure: retry (→ pending, bump retry count) while attempts
24547
+ * remain, else consult the optional supervisor (which can rescue via
24548
+ * retry/reassign/split), else terminal-fail (→ failed). Shared by the
24549
+ * worker-failure, verification-gate, and merge-conflict paths so all three
24550
+ * negotiate the same retry budget and emit the same events.
24551
+ */
24552
+ async applyTaskFailure(taskId, subagentId, errMsg) {
24553
+ const currentRetries = this.retryMap.get(taskId) ?? 0;
24554
+ if (currentRetries < this.maxRetries) {
24555
+ this.retryMap.set(taskId, currentRetries + 1);
24556
+ this.persistRetries(taskId, currentRetries + 1);
24557
+ this.opts.tracker.updateNodeStatus(
24558
+ taskId,
24559
+ "pending",
24560
+ `Retry ${currentRetries + 1}/${this.maxRetries}: ${errMsg}`
24561
+ );
24562
+ this.emit("sdd.task.retrying", {
24563
+ runId: this.runId,
24564
+ taskId,
24565
+ attempt: currentRetries + 1,
24566
+ maxRetries: this.maxRetries
24567
+ });
24568
+ return;
24569
+ }
24570
+ if (await this.trySupervisorRescue(taskId, errMsg)) return;
24571
+ this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
24572
+ this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId, error: errMsg });
24573
+ }
24574
+ /**
24575
+ * Consult `superviseFailure` for a task that has exhausted its retries.
24576
+ * Applies the verdict (retry / reassign+retry / split) and returns true when
24577
+ * the task was rescued (caller must NOT terminal-fail it). Bounded per task by
24578
+ * `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
24579
+ */
24580
+ async trySupervisorRescue(taskId, errMsg) {
24581
+ const supervise = this.opts.superviseFailure;
24582
+ if (!supervise) return false;
24583
+ const used = this.supervisorEscalations.get(taskId) ?? 0;
24584
+ if (used >= this.maxSupervisorEscalations) return false;
24585
+ const node = this.opts.tracker.getNode(taskId);
24586
+ if (!node) return false;
24587
+ let verdict;
24588
+ try {
24589
+ verdict = await supervise({ task: node, error: errMsg, attempts: used });
24590
+ } catch {
24591
+ return false;
24592
+ }
24593
+ if (!verdict || verdict.action === "fail") return false;
24594
+ this.supervisorEscalations.set(taskId, used + 1);
24595
+ const requeue = (reason) => {
24596
+ this.retryMap.delete(taskId);
24597
+ this.persistRetries(taskId, 0);
24598
+ this.opts.tracker.updateNodeStatus(taskId, "pending", reason);
24599
+ };
24600
+ if (verdict.action === "reassign") {
24601
+ this.setTaskModel(taskId, verdict.model, verdict.provider);
24602
+ requeue(`supervisor reassign: ${verdict.model ?? "default"}`);
24603
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "reassign" });
24604
+ return true;
24605
+ }
24606
+ if (verdict.action === "split") {
24607
+ const ids = this.splitTask(taskId, verdict.subtasks);
24608
+ if (ids.length === 0) return false;
24609
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "split" });
24610
+ return true;
24611
+ }
24612
+ requeue("supervisor retry");
24613
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "retry" });
24614
+ return true;
24615
+ }
24616
+ /**
24617
+ * Integrate a verified-successful task's worktree into the base branch.
24618
+ * Commits, squash-merges (optionally running `conflictResolver` first), and on
24619
+ * success releases the worktree. On an UNRESOLVED conflict it returns
24620
+ * `{ok:false}` with the conflicting files so the caller routes the task into
24621
+ * the failure path (a retry forks a fresh worktree off the now-advanced base,
24622
+ * which usually clears the conflict). No-op `{ok:true}` when worktrees are
24623
+ * disabled or none was allocated for this task. Never throws — a merge hiccup
24624
+ * degrades to a (retryable) failure rather than wedging the run.
24625
+ */
24626
+ async integrateWorktree(task, result) {
24627
+ const wt = this.opts.worktrees;
24628
+ if (!wt) return { ok: true };
24629
+ const handle = this.taskWorktrees.get(task.id);
24630
+ if (!handle) return { ok: true };
24631
+ try {
24632
+ await wt.commitAll(handle, `sdd(${task.title}): ${task.id}`);
24633
+ const baseShaBefore = await wt.baseHead(handle);
24634
+ const baseSha = this.opts.conflictResolver ? baseShaBefore : null;
24635
+ const res = await wt.merge(handle, {
24636
+ squash: true,
24637
+ ...this.opts.conflictResolver ? {
24638
+ resolve: (info) => this.opts.conflictResolver({ task, conflictFiles: info.conflictFiles, cwd: info.cwd })
24639
+ } : {}
24640
+ });
24641
+ if (res.ok) {
24642
+ if (res.resolved && this.opts.verifyTask && baseSha) {
24643
+ let regressed;
24644
+ try {
24645
+ const verdict = await this.opts.verifyTask({
24646
+ task,
24647
+ result: result ?? {},
24648
+ cwd: this.opts.projectRoot
24649
+ });
24650
+ if (!verdict.ok) regressed = verdict.reason ?? "verification failed after conflict resolution";
24651
+ } catch (err) {
24652
+ regressed = `verification error after conflict resolution: ${String(err)}`;
24653
+ }
24654
+ if (regressed) {
24655
+ await wt.revertBaseTo(handle, baseSha).catch(() => {
24656
+ });
24657
+ await wt.release(handle, { keep: false }).catch(() => {
24658
+ });
24659
+ this.forgetWorktree(task.id, { keepBranchLabel: true });
24660
+ return { ok: false, conflictFiles: [], reason: regressed };
24661
+ }
24662
+ }
24663
+ const baseShaAfter = await wt.baseHead(handle);
24664
+ if (baseShaAfter && baseShaAfter !== baseShaBefore) {
24665
+ this.mergedCommits.push({ taskId: task.id, sha: baseShaAfter, title: task.title });
24666
+ this.emit("sdd.task.merged", { runId: this.runId, taskId: task.id, sha: baseShaAfter });
24667
+ }
24668
+ await wt.release(handle, { keep: false });
24669
+ this.forgetWorktree(task.id);
24670
+ return { ok: true };
24671
+ }
24672
+ await wt.release(handle, { keep: false }).catch(() => {
24673
+ });
24674
+ this.forgetWorktree(task.id, { keepBranchLabel: true });
24675
+ return { ok: false, conflictFiles: res.conflictFiles ?? [] };
24676
+ } catch {
24677
+ this.forgetWorktree(task.id);
24678
+ return { ok: false, conflictFiles: [] };
24679
+ }
24680
+ }
24681
+ /** Allocate a fresh git worktree per task in the batch (no-op without a manager). */
24682
+ async allocateWorktrees(tasks) {
24683
+ const wt = this.opts.worktrees;
24684
+ if (!wt) return;
24685
+ for (const task of tasks) {
24686
+ if (this.taskWorktrees.has(task.id)) continue;
24687
+ try {
24688
+ const handle = await wt.allocate(`sdd-${task.id}`, {
24689
+ slugHint: task.title,
24690
+ ownerLabel: task.title
24691
+ });
24692
+ if (handle.status === "active") {
24693
+ this.taskWorktrees.set(task.id, handle);
24694
+ this.taskCwds.set(task.id, handle.dir);
24695
+ this.taskBranches.set(task.id, handle.branch);
24696
+ const node = this.opts.tracker.getNode(task.id);
24697
+ if (node) node.metadata = { ...node.metadata, worktreeBranch: handle.branch };
24698
+ }
24699
+ } catch {
24700
+ }
24701
+ }
24702
+ }
24703
+ /**
24704
+ * Resolve each task's worktree after its result is known. Serialized merges
24705
+ * (one at a time) keep the base branch consistent; the wave structure already
24706
+ * guarantees dependency order (a task's blockers merged in an earlier wave).
24707
+ */
24708
+ async resolveWorktrees(tasks) {
24709
+ const wt = this.opts.worktrees;
24710
+ if (!wt) return;
24711
+ for (const task of tasks) {
24712
+ const handle = this.taskWorktrees.get(task.id);
24713
+ if (!handle) continue;
24714
+ const node = this.opts.tracker.getNode(task.id);
24715
+ const status = node?.status;
24716
+ const cancelled = Boolean(node?.metadata?.cancelled);
24717
+ try {
24718
+ if (cancelled) {
24719
+ await wt.release(handle, { keep: false });
24720
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
24721
+ } else if (status === "completed") {
24722
+ await wt.commitAll(handle, `sdd(${task.title}): ${task.id}`);
24723
+ await wt.merge(handle, { squash: true });
24724
+ await wt.release(handle, { keep: false });
24725
+ this.forgetWorktree(task.id);
24726
+ } else if (status === "failed") {
24727
+ await wt.release(handle, { keep: false });
24728
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
23731
24729
  } else {
23732
- this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
24730
+ await wt.release(handle, { keep: false });
24731
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
23733
24732
  }
24733
+ } catch {
24734
+ this.forgetWorktree(task.id);
23734
24735
  }
23735
24736
  }
23736
- return {
23737
- wave,
23738
- batch,
23739
- results,
23740
- successCount,
23741
- failCount,
23742
- durationMs: Date.now() - waveStart,
23743
- stopRequested: this.stopRequested
23744
- };
24737
+ }
24738
+ forgetWorktree(taskId, opts = {}) {
24739
+ this.taskWorktrees.delete(taskId);
24740
+ this.taskCwds.delete(taskId);
24741
+ if (!opts.keepBranchLabel) this.taskBranches.delete(taskId);
24742
+ }
24743
+ /** Persist a task's retry count into node metadata (survives crash → resume). */
24744
+ persistRetries(taskId, retries) {
24745
+ const node = this.opts.tracker.getNode(taskId);
24746
+ if (node) node.metadata = { ...node.metadata, retries };
23745
24747
  }
23746
24748
  buildProgress() {
23747
24749
  const gp = this.opts.tracker.getProgress();
@@ -23760,6 +24762,1611 @@ var SddParallelRun = class {
23760
24762
  }
23761
24763
  };
23762
24764
 
24765
+ // src/core/fallback-model.ts
24766
+ function parseModelRef(ref) {
24767
+ const trimmed = ref.trim();
24768
+ const slash = trimmed.indexOf("/");
24769
+ if (slash !== -1) {
24770
+ return {
24771
+ provider: trimmed.slice(0, slash) || void 0,
24772
+ model: trimmed.slice(slash + 1).trim()
24773
+ };
24774
+ }
24775
+ const parts = trimmed.split(/\s+/);
24776
+ if (parts.length >= 2) {
24777
+ return { provider: parts[0], model: parts.slice(1).join(" ") };
24778
+ }
24779
+ return { model: trimmed };
24780
+ }
24781
+
24782
+ // src/sdd/sdd-supervisor.ts
24783
+ var SddSupervisor = class {
24784
+ constructor(opts) {
24785
+ this.opts = opts;
24786
+ }
24787
+ opts;
24788
+ /**
24789
+ * Bind this as `SddParallelRunOptions.superviseFailure`. Returns a verdict the
24790
+ * run applies, or `undefined`/`{action:'fail'}` to let the task terminal-fail.
24791
+ */
24792
+ superviseFailure = async (info) => {
24793
+ const { task, error, attempts } = info;
24794
+ const canReassign = (this.opts.reassignModels?.length ?? 0) > 0;
24795
+ const canSplit = Boolean(this.opts.generateSubtasks);
24796
+ const decision = await this.opts.brain.decide({
24797
+ id: `sdd-supervisor-${task.id}-${attempts}`,
24798
+ source: "system",
24799
+ question: `SDD task "${task.title}" exhausted its retries. How should the run proceed?`,
24800
+ context: `Error: ${error}
24801
+ Supervisor rescues already used: ${attempts}`,
24802
+ options: [
24803
+ { id: "retry", label: "Retry the task as-is", recommended: true },
24804
+ ...canReassign ? [{ id: "reassign", label: "Reassign to a different model" }] : [],
24805
+ ...canSplit ? [{ id: "split", label: "Split into smaller sub-tasks" }] : [],
24806
+ { id: "fail", label: "Give up and mark the task failed" }
24807
+ ],
24808
+ // Higher risk once we've already rescued it once — pushes a wired LLM/human
24809
+ // toward a decisive verdict instead of looping retries.
24810
+ risk: attempts >= 1 ? "high" : "medium",
24811
+ // `continue` → policy answers in place (bounded retry, LLM never runs).
24812
+ // `ask_human` → policy escalates so the autonomous LLM layer can actually
24813
+ // pick reassign/split (see requestLlmVerdict's safety contract).
24814
+ fallback: this.opts.requestLlmVerdict ? "ask_human" : "continue"
24815
+ });
24816
+ if (decision.type === "deny") return { action: "fail" };
24817
+ if (decision.type !== "answer") return { action: "retry" };
24818
+ const choice = decision.optionId ?? "retry";
24819
+ if (choice === "fail") return { action: "fail" };
24820
+ if (choice === "reassign" && canReassign) {
24821
+ const models = this.opts.reassignModels;
24822
+ const ref = models[attempts % models.length];
24823
+ const parsed = ref ? parseModelRef(ref) : void 0;
24824
+ return { action: "reassign", model: parsed?.model, provider: parsed?.provider };
24825
+ }
24826
+ if (choice === "split" && this.opts.generateSubtasks) {
24827
+ const subtasks = await this.opts.generateSubtasks({ task, error }).catch(() => []);
24828
+ return subtasks.length ? { action: "split", subtasks } : { action: "retry" };
24829
+ }
24830
+ return { action: "retry" };
24831
+ };
24832
+ };
24833
+ function makeCommandVerifier(options = {}) {
24834
+ const metadataKey = options.metadataKey ?? "verificationCommand";
24835
+ const timeoutMs = options.timeoutMs ?? 18e4;
24836
+ return async function verifyTask(info) {
24837
+ const cmd = info.task.metadata?.[metadataKey];
24838
+ if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
24839
+ return await new Promise((resolve8) => {
24840
+ const child = spawn(cmd, { cwd: info.cwd, shell: true, windowsHide: true, stdio: "ignore" });
24841
+ const timer = setTimeout(() => {
24842
+ child.kill();
24843
+ resolve8({ ok: false, reason: `verification timed out: ${cmd}` });
24844
+ }, timeoutMs);
24845
+ child.on("exit", (code) => {
24846
+ clearTimeout(timer);
24847
+ resolve8(
24848
+ code === 0 ? { ok: true } : { ok: false, reason: `verification failed (exit ${code}): ${cmd}` }
24849
+ );
24850
+ });
24851
+ child.on("error", (err) => {
24852
+ clearTimeout(timer);
24853
+ resolve8({ ok: false, reason: `verification spawn error: ${String(err)}` });
24854
+ });
24855
+ });
24856
+ };
24857
+ }
24858
+
24859
+ // src/sdd/decompose-task.ts
24860
+ var TASK_TYPES = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
24861
+ var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
24862
+ function extractJsonArray(text) {
24863
+ const fence = text.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
24864
+ if (fence?.[1]) return fence[1].trim();
24865
+ const bare = text.match(/(\[[\s\S]*\])/);
24866
+ if (bare?.[1]) {
24867
+ try {
24868
+ if (Array.isArray(JSON.parse(bare[1]))) return bare[1];
24869
+ } catch {
24870
+ }
24871
+ }
24872
+ return null;
24873
+ }
24874
+ function buildPrompt(task, error, min, max) {
24875
+ return [
24876
+ "You are an engineering lead triaging a software task that FAILED after every",
24877
+ "automated retry was exhausted. Break it into smaller, independently-executable",
24878
+ `sub-tasks (between ${min} and ${max}) so separate workers can each tackle a`,
24879
+ "narrower slice. Each sub-task must be strictly smaller than the parent \u2014 never",
24880
+ "restate the whole task as one sub-task.",
24881
+ "",
24882
+ `Parent task title: ${task.title}`,
24883
+ `Parent description: ${task.description}`,
24884
+ `Failure / error: ${error || "(none recorded)"}`,
24885
+ "",
24886
+ "Respond with ONLY a JSON array (no prose) of objects with this shape:",
24887
+ '[{"title": "...", "description": "...", "type": "feature|bugfix|refactor|docs|test|chore", "priority": "critical|high|medium|low"}]',
24888
+ "`type` and `priority` are optional (they default to the parent's)."
24889
+ ].join("\n");
24890
+ }
24891
+ function makeLlmSubtaskGenerator(opts) {
24892
+ const min = Math.max(2, opts.minSubtasks ?? 2);
24893
+ const max = Math.max(min, opts.maxSubtasks ?? 4);
24894
+ return async function generateSubtasks(info) {
24895
+ let text;
24896
+ try {
24897
+ text = await opts.run(buildPrompt(info.task, info.error, min, max));
24898
+ } catch {
24899
+ return [];
24900
+ }
24901
+ const json = extractJsonArray(text ?? "");
24902
+ if (!json) return [];
24903
+ let raw;
24904
+ try {
24905
+ raw = JSON.parse(json);
24906
+ } catch {
24907
+ return [];
24908
+ }
24909
+ if (!Array.isArray(raw)) return [];
24910
+ const specs = [];
24911
+ for (const item of raw) {
24912
+ if (!item || typeof item !== "object") continue;
24913
+ const r = item;
24914
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
24915
+ const description = typeof r["description"] === "string" ? r["description"].trim() : "";
24916
+ if (!title || !description) continue;
24917
+ const type = TASK_TYPES.has(r["type"]) ? r["type"] : void 0;
24918
+ const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
24919
+ specs.push({ title, description, type, priority });
24920
+ if (specs.length >= max) break;
24921
+ }
24922
+ return specs.length >= min ? specs : [];
24923
+ };
24924
+ }
24925
+ var START = "<<<<<<<";
24926
+ var BASE = "|||||||";
24927
+ var SEP2 = "=======";
24928
+ var END = ">>>>>>>";
24929
+ function resolveConflictText(text, side) {
24930
+ const out = [];
24931
+ let state = "normal";
24932
+ for (const line of text.split("\n")) {
24933
+ const marker = line.slice(0, 7);
24934
+ if (state === "normal" && marker === START) {
24935
+ state = "ours";
24936
+ continue;
24937
+ }
24938
+ if (state !== "normal" && marker === BASE) {
24939
+ state = "base";
24940
+ continue;
24941
+ }
24942
+ if (state !== "normal" && marker === SEP2) {
24943
+ state = "theirs";
24944
+ continue;
24945
+ }
24946
+ if (state !== "normal" && marker === END) {
24947
+ state = "normal";
24948
+ continue;
24949
+ }
24950
+ if (state === "normal") out.push(line);
24951
+ else if (state === "ours" && side === "base") out.push(line);
24952
+ else if (state === "theirs" && side === "incoming") out.push(line);
24953
+ }
24954
+ return out.join("\n");
24955
+ }
24956
+ function hasConflictMarkers(text) {
24957
+ return text.split("\n").some((l) => {
24958
+ const m = l.slice(0, 7);
24959
+ return m === START || m === SEP2 || m === END || m === BASE;
24960
+ });
24961
+ }
24962
+ function makePreferSideConflictResolver(side) {
24963
+ return async function conflictResolver(info) {
24964
+ if (info.conflictFiles.length === 0) return false;
24965
+ for (const rel of info.conflictFiles) {
24966
+ const abs = isAbsolute(rel) ? rel : join(info.cwd, rel);
24967
+ let content;
24968
+ try {
24969
+ content = await readFile(abs, "utf8");
24970
+ } catch {
24971
+ return false;
24972
+ }
24973
+ const resolved = resolveConflictText(content, side);
24974
+ if (hasConflictMarkers(resolved)) return false;
24975
+ try {
24976
+ await writeFile(abs, resolved, "utf8");
24977
+ } catch {
24978
+ return false;
24979
+ }
24980
+ }
24981
+ return true;
24982
+ };
24983
+ }
24984
+ function unfence(text) {
24985
+ const m = text.match(/^[\s\S]*?```[^\n]*\n([\s\S]*?)\n```[\s\S]*$/);
24986
+ return m?.[1] !== void 0 ? m[1] : text.trim();
24987
+ }
24988
+ function nonMarkerLineCount(text) {
24989
+ return text.split("\n").filter((l) => {
24990
+ const m = l.slice(0, 7);
24991
+ return m !== START && m !== SEP2 && m !== END && m !== BASE;
24992
+ }).length;
24993
+ }
24994
+ function makeLlmConflictResolver(opts) {
24995
+ const minFraction = opts.minRetainedFraction ?? 0.5;
24996
+ return async function conflictResolver(info) {
24997
+ if (info.conflictFiles.length === 0) return false;
24998
+ for (const rel of info.conflictFiles) {
24999
+ const abs = isAbsolute(rel) ? rel : join(info.cwd, rel);
25000
+ let content;
25001
+ try {
25002
+ content = await readFile(abs, "utf8");
25003
+ } catch {
25004
+ return false;
25005
+ }
25006
+ if (!hasConflictMarkers(content)) continue;
25007
+ const prompt = [
25008
+ "You are resolving a git MERGE CONFLICT in a single file. Below is the",
25009
+ "full file with conflict markers (<<<<<<<, =======, >>>>>>>, and possibly",
25010
+ "||||||| for diff3). Combine both sides into the correct, complete file \u2014",
25011
+ "keep ALL non-conflicting content verbatim and reconcile each hunk sensibly.",
25012
+ "Return ONLY the fully resolved file contents (no conflict markers, no",
25013
+ "commentary), optionally wrapped in a single ``` code fence.",
25014
+ "",
25015
+ `File: ${rel}`,
25016
+ "--- BEGIN ---",
25017
+ content,
25018
+ "--- END ---"
25019
+ ].join("\n");
25020
+ let out;
25021
+ try {
25022
+ out = await opts.run(prompt);
25023
+ } catch {
25024
+ return false;
25025
+ }
25026
+ const resolved = unfence(out ?? "");
25027
+ if (!resolved.trim() || hasConflictMarkers(resolved)) return false;
25028
+ if (resolved.split("\n").length < Math.floor(nonMarkerLineCount(content) * minFraction)) {
25029
+ return false;
25030
+ }
25031
+ try {
25032
+ await writeFile(abs, resolved, "utf8");
25033
+ } catch {
25034
+ return false;
25035
+ }
25036
+ }
25037
+ return true;
25038
+ };
25039
+ }
25040
+
25041
+ // src/sdd/board-types.ts
25042
+ function shortIdMap(graph) {
25043
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
25044
+ const m = /* @__PURE__ */ new Map();
25045
+ nodes.forEach((n, i) => {
25046
+ m.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
25047
+ });
25048
+ return m;
25049
+ }
25050
+ function buildBoardTasks(graph) {
25051
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
25052
+ const shortId = shortIdMap(graph);
25053
+ const blockers = /* @__PURE__ */ new Map();
25054
+ for (const n of nodes) blockers.set(n.id, []);
25055
+ for (const e of graph.edges) {
25056
+ if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
25057
+ }
25058
+ const statusOf = (id) => graph.nodes.get(id)?.status;
25059
+ const depthCache = /* @__PURE__ */ new Map();
25060
+ const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
25061
+ const cached = depthCache.get(id);
25062
+ if (cached !== void 0) return cached;
25063
+ if (seen.has(id)) return 0;
25064
+ seen.add(id);
25065
+ const deps = blockers.get(id) ?? [];
25066
+ const d = deps.length === 0 ? 0 : 1 + Math.max(...deps.map((b) => depthOf(b, seen)));
25067
+ depthCache.set(id, d);
25068
+ return d;
25069
+ };
25070
+ const toTask = (n) => {
25071
+ const deps = blockers.get(n.id) ?? [];
25072
+ const allDepsDone = deps.every((b) => statusOf(b) === "completed");
25073
+ const meta = n.metadata ?? {};
25074
+ const cancelled = Boolean(meta["cancelled"]);
25075
+ const displayStatus = cancelled ? "cancelled" : n.status === "pending" && deps.length > 0 && allDepsDone ? "queued" : n.status;
25076
+ return {
25077
+ id: n.id,
25078
+ shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
25079
+ title: n.title,
25080
+ description: n.description,
25081
+ status: n.status,
25082
+ displayStatus,
25083
+ priority: n.priority,
25084
+ type: n.type,
25085
+ deps: deps.map((b) => shortId.get(b) ?? b.slice(0, 6)),
25086
+ agentName: n.assignee,
25087
+ worktreeBranch: typeof meta["worktreeBranch"] === "string" ? meta["worktreeBranch"] : void 0,
25088
+ startedAt: n.startedAt,
25089
+ completedAt: n.completedAt,
25090
+ retries: typeof meta["retries"] === "number" ? meta["retries"] : 0,
25091
+ model: typeof meta["model"] === "string" ? meta["model"] : void 0,
25092
+ provider: typeof meta["provider"] === "string" ? meta["provider"] : void 0,
25093
+ fallbackModels: Array.isArray(meta["fallbackModels"]) ? meta["fallbackModels"] : void 0,
25094
+ verificationCommand: typeof meta["verificationCommand"] === "string" ? meta["verificationCommand"] : void 0
25095
+ };
25096
+ };
25097
+ const tasks = nodes.map(toTask);
25098
+ const byDepth = /* @__PURE__ */ new Map();
25099
+ for (const n of nodes) {
25100
+ const d = depthOf(n.id);
25101
+ if (!byDepth.has(d)) byDepth.set(d, []);
25102
+ byDepth.get(d)?.push(shortId.get(n.id) ?? n.id.slice(0, 6));
25103
+ }
25104
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, taskIds: byDepth.get(d) ?? [] }));
25105
+ return { tasks, columns };
25106
+ }
25107
+ function buildBoardSnapshot(graph, run, now) {
25108
+ const { tasks, columns } = buildBoardTasks(graph);
25109
+ return {
25110
+ runId: run.runId,
25111
+ specId: run.specId,
25112
+ graphId: graph.id,
25113
+ title: graph.title,
25114
+ status: run.status,
25115
+ startedAt: run.startedAt,
25116
+ updatedAt: now,
25117
+ progress: computeTaskProgress(graph),
25118
+ wave: run.wave,
25119
+ tasks,
25120
+ columns,
25121
+ diagnostics: run.deadlockChains?.length ? { deadlockChains: run.deadlockChains } : void 0,
25122
+ defaultModel: run.defaultModel,
25123
+ defaultProvider: run.defaultProvider,
25124
+ fallbackModels: run.fallbackModels,
25125
+ baseBranch: run.baseBranch,
25126
+ mergedCommits: run.mergedCommits?.length ? run.mergedCommits : void 0
25127
+ };
25128
+ }
25129
+
25130
+ // src/sdd/sdd-board-store.ts
25131
+ init_atomic_write();
25132
+ var SddBoardStore = class {
25133
+ baseDir;
25134
+ indexPath;
25135
+ constructor(opts) {
25136
+ this.baseDir = opts.baseDir;
25137
+ this.indexPath = path4.join(this.baseDir, "_index.json");
25138
+ }
25139
+ snapshotPath(runId) {
25140
+ return path4.join(this.baseDir, `${this.safe(runId)}.json`);
25141
+ }
25142
+ eventsPath(runId) {
25143
+ return path4.join(this.baseDir, `${this.safe(runId)}.events.jsonl`);
25144
+ }
25145
+ controlPath(runId) {
25146
+ return path4.join(this.baseDir, `${this.safe(runId)}.control.jsonl`);
25147
+ }
25148
+ async saveSnapshot(snapshot) {
25149
+ await ensureDir(this.baseDir);
25150
+ await atomicWrite(this.snapshotPath(snapshot.runId), JSON.stringify(snapshot, null, 2), {
25151
+ mode: 384
25152
+ });
25153
+ await this.updateIndex(snapshot);
25154
+ }
25155
+ async load(runId) {
25156
+ try {
25157
+ const raw = await fsp2.readFile(this.snapshotPath(runId), "utf8");
25158
+ return JSON.parse(raw);
25159
+ } catch {
25160
+ return null;
25161
+ }
25162
+ }
25163
+ async list() {
25164
+ const index = await this.readIndex();
25165
+ return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
25166
+ }
25167
+ async loadLatestForSpec(specId) {
25168
+ const entry = (await this.list()).find((e) => e.specId === specId);
25169
+ return entry ? this.load(entry.runId) : null;
25170
+ }
25171
+ /** Append one line to the board's JSONL event log (best-effort, never throws). */
25172
+ async appendEvent(runId, event) {
25173
+ try {
25174
+ await ensureDir(this.baseDir);
25175
+ await fsp2.appendFile(this.eventsPath(runId), `${JSON.stringify(event)}
25176
+ `, { mode: 384 });
25177
+ } catch {
25178
+ }
25179
+ }
25180
+ /** Append a control command (used by readers to steer a CLI-owned run). */
25181
+ async appendControl(runId, command) {
25182
+ await ensureDir(this.baseDir);
25183
+ await fsp2.appendFile(this.controlPath(runId), `${JSON.stringify(command)}
25184
+ `, { mode: 384 });
25185
+ }
25186
+ /** Read + truncate the control queue (the run drains it). Returns parsed commands. */
25187
+ async drainControl(runId) {
25188
+ const p = this.controlPath(runId);
25189
+ let raw;
25190
+ try {
25191
+ raw = await fsp2.readFile(p, "utf8");
25192
+ } catch {
25193
+ return [];
25194
+ }
25195
+ try {
25196
+ await fsp2.writeFile(p, "", { mode: 384 });
25197
+ } catch {
25198
+ }
25199
+ return raw.split("\n").filter((l) => l.trim()).map((l) => {
25200
+ try {
25201
+ return JSON.parse(l);
25202
+ } catch {
25203
+ return null;
25204
+ }
25205
+ }).filter((c) => c !== null);
25206
+ }
25207
+ async delete(runId) {
25208
+ await Promise.allSettled([
25209
+ fsp2.unlink(this.snapshotPath(runId)),
25210
+ fsp2.unlink(this.eventsPath(runId)),
25211
+ fsp2.unlink(this.controlPath(runId))
25212
+ ]);
25213
+ await this.removeFromIndex(runId);
25214
+ }
25215
+ // ── internal ────────────────────────────────────────────────────────────
25216
+ safe(runId) {
25217
+ return runId.replace(/[^a-zA-Z0-9._-]/g, "_");
25218
+ }
25219
+ async readIndex() {
25220
+ try {
25221
+ const raw = await fsp2.readFile(this.indexPath, "utf8");
25222
+ const parsed = JSON.parse(raw);
25223
+ if (parsed?.version === 1) return parsed;
25224
+ } catch {
25225
+ }
25226
+ return { version: 1, entries: [] };
25227
+ }
25228
+ async updateIndex(snapshot) {
25229
+ const index = await this.readIndex();
25230
+ const entry = {
25231
+ runId: snapshot.runId,
25232
+ specId: snapshot.specId,
25233
+ title: snapshot.title,
25234
+ status: snapshot.status,
25235
+ total: snapshot.progress.total,
25236
+ completed: snapshot.progress.completed,
25237
+ updatedAt: snapshot.updatedAt
25238
+ };
25239
+ const idx = index.entries.findIndex((e) => e.runId === snapshot.runId);
25240
+ if (idx >= 0) index.entries[idx] = entry;
25241
+ else index.entries.push(entry);
25242
+ await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
25243
+ }
25244
+ async removeFromIndex(runId) {
25245
+ const index = await this.readIndex();
25246
+ index.entries = index.entries.filter((e) => e.runId !== runId);
25247
+ await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
25248
+ }
25249
+ };
25250
+
25251
+ // src/sdd/sdd-board-projector.ts
25252
+ var SddBoardProjector = class _SddBoardProjector {
25253
+ o;
25254
+ now;
25255
+ throttleMs;
25256
+ shortId;
25257
+ status = "idle";
25258
+ wave = 0;
25259
+ startedAt;
25260
+ deadlockChains = [];
25261
+ /** Live activity feed, most recent first (capped). */
25262
+ feed = [];
25263
+ static FEED_CAP = 60;
25264
+ finished = false;
25265
+ runDeadlocked = false;
25266
+ runStopped = false;
25267
+ /** Squash commits the run landed on the base branch (for post-run rollback). */
25268
+ mergedCommits = [];
25269
+ /** Base branch reported by the run at start (overrides the constructor option). */
25270
+ runBaseBranch;
25271
+ dirty = false;
25272
+ timer = null;
25273
+ unsubs = [];
25274
+ /** Tail of in-flight persistence, so callers can await a settled state. */
25275
+ lastSave = Promise.resolve();
25276
+ constructor(opts) {
25277
+ this.o = opts;
25278
+ this.now = opts.now ?? Date.now;
25279
+ this.throttleMs = opts.throttleMs ?? 250;
25280
+ this.shortId = shortIdMap(opts.graph);
25281
+ this.startedAt = this.now();
25282
+ this.unsubs.push(opts.tracker.subscribe(() => this.markDirty()));
25283
+ this.onRun("sdd.run.started", (e) => {
25284
+ this.status = "running";
25285
+ this.startedAt = this.now();
25286
+ if (e.baseBranch) this.runBaseBranch = e.baseBranch;
25287
+ this.markDirty();
25288
+ });
25289
+ this.onRun("sdd.run.finished", (e) => {
25290
+ this.finished = true;
25291
+ this.runDeadlocked = e.deadlocked;
25292
+ this.runStopped = e.stopped;
25293
+ this.flush();
25294
+ });
25295
+ this.onRun("sdd.wave", (e) => {
25296
+ this.wave = e.wave;
25297
+ this.pushFeed({ ts: this.now(), kind: "wave", text: `Wave ${e.wave + 1} started \xB7 ${e.batchSize} task(s) in parallel` });
25298
+ this.markDirty();
25299
+ });
25300
+ this.onRun("sdd.deadlock", (e) => {
25301
+ this.deadlockChains = e.chains.map((c) => ({
25302
+ blocked: this.shortId.get(c.blocked) ?? c.blocked.slice(0, 6),
25303
+ blockedBy: c.blockedBy.map((b) => this.shortId.get(b) ?? b.slice(0, 6))
25304
+ }));
25305
+ this.pushFeed({ ts: this.now(), kind: "deadlock", text: `Deadlock \u2014 ${e.chains.length} task(s) blocked by failed work` });
25306
+ this.markDirty();
25307
+ });
25308
+ this.onRun("sdd.task.started", (e) => {
25309
+ const sid = this.shortId.get(e.taskId);
25310
+ this.pushFeed({
25311
+ ts: this.now(),
25312
+ kind: "started",
25313
+ taskShortId: sid,
25314
+ agentName: e.agentName,
25315
+ text: `${e.agentName || "a worker"} picked up ${sid ?? "a task"}${this.titleOf(e.taskId)}`
25316
+ });
25317
+ this.markDirty();
25318
+ });
25319
+ this.onRun("sdd.task.completed", (e) => {
25320
+ const sid = this.shortId.get(e.taskId);
25321
+ const agent = this.assigneeOf(e.taskId);
25322
+ this.pushFeed({
25323
+ ts: this.now(),
25324
+ kind: "completed",
25325
+ taskShortId: sid,
25326
+ agentName: agent,
25327
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} completed${agent ? ` by ${agent}` : ""} \xB7 ${(e.durationMs / 1e3).toFixed(1)}s`
25328
+ });
25329
+ this.markDirty();
25330
+ });
25331
+ this.onRun("sdd.task.failed", (e) => {
25332
+ const sid = this.shortId.get(e.taskId);
25333
+ this.pushFeed({
25334
+ ts: this.now(),
25335
+ kind: "failed",
25336
+ taskShortId: sid,
25337
+ agentName: this.assigneeOf(e.taskId),
25338
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} failed \u2014 ${e.error}`
25339
+ });
25340
+ this.markDirty();
25341
+ });
25342
+ this.onRun("sdd.task.retrying", (e) => {
25343
+ const sid = this.shortId.get(e.taskId);
25344
+ this.pushFeed({
25345
+ ts: this.now(),
25346
+ kind: "retrying",
25347
+ taskShortId: sid,
25348
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} retrying (${e.attempt}/${e.maxRetries})`
25349
+ });
25350
+ this.markDirty();
25351
+ });
25352
+ this.onRun("sdd.task.verification_failed", (e) => {
25353
+ const sid = this.shortId.get(e.taskId);
25354
+ this.pushFeed({
25355
+ ts: this.now(),
25356
+ kind: "verification_failed",
25357
+ taskShortId: sid,
25358
+ agentName: this.assigneeOf(e.taskId),
25359
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} failed verification \u2014 ${e.reason}`
25360
+ });
25361
+ this.markDirty();
25362
+ });
25363
+ this.onRun("sdd.task.conflict", (e) => {
25364
+ const sid = this.shortId.get(e.taskId);
25365
+ const files = e.conflictFiles.length;
25366
+ this.pushFeed({
25367
+ ts: this.now(),
25368
+ kind: "conflict",
25369
+ taskShortId: sid,
25370
+ agentName: this.assigneeOf(e.taskId),
25371
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} merge conflict \u2014 ${files} file(s)${files ? `: ${e.conflictFiles.slice(0, 3).join(", ")}${files > 3 ? "\u2026" : ""}` : ""}`
25372
+ });
25373
+ this.markDirty();
25374
+ });
25375
+ this.onRun("sdd.task.merged", (e) => {
25376
+ const title = this.o.graph.nodes.get(e.taskId)?.title ?? "";
25377
+ this.mergedCommits.push({ taskId: e.taskId, sha: e.sha, title });
25378
+ const sid = this.shortId.get(e.taskId);
25379
+ this.pushFeed({
25380
+ ts: this.now(),
25381
+ kind: "completed",
25382
+ taskShortId: sid,
25383
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} merged \u2192 ${this.runBaseBranch ?? this.o.baseBranch ?? "base"} (${e.sha.slice(0, 8)})`
25384
+ });
25385
+ this.markDirty();
25386
+ });
25387
+ this.onRun("sdd.task.split", (e) => {
25388
+ const sid = this.shortId.get(e.taskId);
25389
+ this.pushFeed({
25390
+ ts: this.now(),
25391
+ kind: "split",
25392
+ taskShortId: sid,
25393
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} split into ${e.subtaskIds.length} sub-task(s)`
25394
+ });
25395
+ this.markDirty();
25396
+ });
25397
+ this.onRun("sdd.supervisor.decision", (e) => {
25398
+ const sid = this.shortId.get(e.taskId);
25399
+ this.pushFeed({
25400
+ ts: this.now(),
25401
+ kind: "supervisor",
25402
+ taskShortId: sid,
25403
+ text: `supervisor \u2192 ${e.action} for ${sid ?? "task"}${this.titleOf(e.taskId)}${e.rationale ? ` (${e.rationale})` : ""}`
25404
+ });
25405
+ this.markDirty();
25406
+ });
25407
+ }
25408
+ pushFeed(entry) {
25409
+ this.feed.unshift(entry);
25410
+ if (this.feed.length > _SddBoardProjector.FEED_CAP) this.feed.length = _SddBoardProjector.FEED_CAP;
25411
+ }
25412
+ /** ` (title…)` suffix for a feed line, or '' when the node/title is missing. */
25413
+ titleOf(taskId) {
25414
+ const t = this.o.graph.nodes.get(taskId)?.title;
25415
+ if (!t) return "";
25416
+ return ` (${t.length > 40 ? `${t.slice(0, 39)}\u2026` : t})`;
25417
+ }
25418
+ assigneeOf(taskId) {
25419
+ return this.o.graph.nodes.get(taskId)?.assignee;
25420
+ }
25421
+ /** Latest snapshot, built on demand (e.g. for a late-joining client). */
25422
+ snapshot() {
25423
+ return this.build();
25424
+ }
25425
+ /** Resolve once all in-flight snapshot persistence has settled. */
25426
+ async drain() {
25427
+ await this.lastSave;
25428
+ }
25429
+ /** Stop projecting and release subscriptions. */
25430
+ dispose() {
25431
+ if (this.timer) {
25432
+ clearTimeout(this.timer);
25433
+ this.timer = null;
25434
+ }
25435
+ for (const u of this.unsubs) u();
25436
+ this.unsubs.length = 0;
25437
+ }
25438
+ // ── internal ────────────────────────────────────────────────────────────
25439
+ /** Subscribe to a run event scoped to this run id; also append to JSONL. */
25440
+ onRun(event, handler) {
25441
+ const wrapped = (e) => {
25442
+ if (e.runId !== this.o.runId) return;
25443
+ void this.o.store?.appendEvent(this.o.runId, { ts: this.now(), type: event, payload: e });
25444
+ handler(e);
25445
+ };
25446
+ const off = this.o.events.on(event, wrapped);
25447
+ this.unsubs.push(off);
25448
+ }
25449
+ resolveStatus(completed, total) {
25450
+ if (!this.finished) return this.status;
25451
+ if (this.runDeadlocked) return "deadlocked";
25452
+ if (total > 0 && completed >= total) return "completed";
25453
+ if (this.runStopped) return "paused";
25454
+ return "failed";
25455
+ }
25456
+ build() {
25457
+ const snap = buildBoardSnapshot(
25458
+ this.o.graph,
25459
+ {
25460
+ runId: this.o.runId,
25461
+ specId: this.o.specId,
25462
+ status: "running",
25463
+ startedAt: this.startedAt,
25464
+ wave: this.wave,
25465
+ deadlockChains: this.deadlockChains,
25466
+ defaultModel: this.o.defaultModel,
25467
+ defaultProvider: this.o.defaultProvider,
25468
+ fallbackModels: this.o.fallbackModels,
25469
+ baseBranch: this.runBaseBranch ?? this.o.baseBranch,
25470
+ mergedCommits: this.mergedCommits
25471
+ },
25472
+ this.now()
25473
+ );
25474
+ snap.status = this.resolveStatus(snap.progress.completed, snap.progress.total);
25475
+ snap.feed = this.feed.slice(0, _SddBoardProjector.FEED_CAP);
25476
+ return snap;
25477
+ }
25478
+ markDirty() {
25479
+ this.dirty = true;
25480
+ if (this.timer || this.finished) return;
25481
+ this.timer = setTimeout(() => {
25482
+ this.timer = null;
25483
+ if (this.dirty) this.flush();
25484
+ }, this.throttleMs);
25485
+ }
25486
+ flush() {
25487
+ this.dirty = false;
25488
+ if (this.timer) {
25489
+ clearTimeout(this.timer);
25490
+ this.timer = null;
25491
+ }
25492
+ const snap = this.build();
25493
+ this.o.events.emit("sdd.board.snapshot", { runId: this.o.runId, snapshot: snap });
25494
+ if (this.o.store) {
25495
+ const store = this.o.store;
25496
+ this.lastSave = this.lastSave.then(() => store.saveSnapshot(snap)).catch(() => {
25497
+ });
25498
+ }
25499
+ }
25500
+ };
25501
+
25502
+ // src/sdd/sdd-run-registry.ts
25503
+ var SddRunRegistry = class {
25504
+ current = null;
25505
+ register(control) {
25506
+ this.current = control;
25507
+ }
25508
+ clear(runId) {
25509
+ if (this.current?.runId === runId) this.current = null;
25510
+ }
25511
+ getActive() {
25512
+ return this.current;
25513
+ }
25514
+ };
25515
+
25516
+ // src/sdd/sdd-interview-driver.ts
25517
+ var SddInterviewDriver = class {
25518
+ builder;
25519
+ o;
25520
+ minQuestions;
25521
+ maxQuestions;
25522
+ tracker = null;
25523
+ graph = null;
25524
+ constructor(opts) {
25525
+ this.o = opts;
25526
+ this.minQuestions = opts.minQuestions ?? 2;
25527
+ this.maxQuestions = opts.maxQuestions ?? 10;
25528
+ this.builder = new AISpecBuilder({
25529
+ store: opts.specStore,
25530
+ sessionPath: opts.sessionPath,
25531
+ projectContext: opts.projectContext,
25532
+ minQuestions: this.minQuestions,
25533
+ maxQuestions: this.maxQuestions
25534
+ });
25535
+ }
25536
+ /** Begin a fresh interview. Returns the first AI prompt (a question kickoff). */
25537
+ start(title, intent) {
25538
+ this.builder.startSession(title, intent);
25539
+ this.tracker = null;
25540
+ this.graph = null;
25541
+ return this.builder.getAIPrompt();
25542
+ }
25543
+ /**
25544
+ * Resume a previously-persisted interview from disk. Re-hydrates the task
25545
+ * graph too when one was already produced. Returns true if a session loaded.
25546
+ */
25547
+ async loadExisting() {
25548
+ const loaded = await this.builder.loadSession();
25549
+ if (!loaded) return false;
25550
+ const graphId = this.builder.getTaskGraphId();
25551
+ if (graphId) {
25552
+ const graph = await this.o.graphStore.load(graphId);
25553
+ if (graph) {
25554
+ this.graph = graph;
25555
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
25556
+ tracker.setGraph(graph);
25557
+ this.tracker = tracker;
25558
+ }
25559
+ }
25560
+ return true;
25561
+ }
25562
+ phase() {
25563
+ return this.builder.getPhase();
25564
+ }
25565
+ currentPrompt() {
25566
+ return this.builder.getAIPrompt();
25567
+ }
25568
+ getTracker() {
25569
+ return this.tracker;
25570
+ }
25571
+ getGraph() {
25572
+ return this.graph;
25573
+ }
25574
+ /** Record a Q/A pair (the agent asked `question`, the user replied `answer`). */
25575
+ submitAnswer(question, answer) {
25576
+ this.builder.addAnswer(question, answer);
25577
+ }
25578
+ /**
25579
+ * Feed the agent's text output back into the interview. Detects, in order:
25580
+ * 1. a Specification JSON → setSpec (phase → spec_review) + persist to SpecStore
25581
+ * 2. an implementation plan (implementation phase) → setImplementation
25582
+ * 3. a task JSON array → build + persist a TaskGraph
25583
+ * Each step is independent and best-effort; a malformed payload is ignored
25584
+ * rather than thrown, so a chatty agent turn never breaks the interview.
25585
+ */
25586
+ async ingestAgentOutput(text) {
25587
+ const result = {
25588
+ specDetected: false,
25589
+ implementationDetected: false,
25590
+ tasksDetected: false
25591
+ };
25592
+ if (!this.builder.getSession().spec) {
25593
+ const spec = this.builder.tryParseSpecFromOutput(text);
25594
+ if (spec) {
25595
+ this.builder.setSpec(spec);
25596
+ await this.persistSpec(spec);
25597
+ result.specDetected = true;
25598
+ }
25599
+ }
25600
+ if (this.builder.getPhase() === "implementation") {
25601
+ if (this.trySaveImplementationPlan(text)) result.implementationDetected = true;
25602
+ }
25603
+ const session = this.builder.getSession();
25604
+ if (session.spec) {
25605
+ const built = await this.tryBuildTasksFromOutput(text);
25606
+ if (built) {
25607
+ result.tasksDetected = true;
25608
+ result.graphId = built;
25609
+ }
25610
+ }
25611
+ return result;
25612
+ }
25613
+ /**
25614
+ * Advance to the next phase (mirrors `/sdd approve`). When moving into the
25615
+ * executing phase, guarantees a task graph exists — deterministically
25616
+ * generating one from the approved spec if the agent never emitted a valid
25617
+ * task array. Returns the new phase and its AI prompt.
25618
+ */
25619
+ async approve() {
25620
+ const phase = this.builder.approve();
25621
+ if (phase === "executing") {
25622
+ await this.ensureTaskGraph();
25623
+ }
25624
+ return { phase, prompt: this.builder.getAIPrompt() };
25625
+ }
25626
+ /**
25627
+ * Ensure a TaskGraph exists for the approved spec. If the agent already
25628
+ * produced one (via `ingestAgentOutput`), returns it; otherwise builds a
25629
+ * deterministic graph from the spec's requirements via TaskGenerator. This is
25630
+ * the robustness backstop: a run can always start, even if the model never
25631
+ * emitted a parseable task array.
25632
+ */
25633
+ async ensureTaskGraph() {
25634
+ if (this.graph) return this.graph;
25635
+ const spec = this.builder.getSession().spec;
25636
+ if (!spec) return null;
25637
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
25638
+ const generator = new TaskGenerator({
25639
+ taskTracker: tracker,
25640
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
25641
+ });
25642
+ const graph = await generator.generateFromSpec(spec);
25643
+ this.tracker = tracker;
25644
+ this.graph = graph;
25645
+ await this.persistGraph(graph);
25646
+ this.builder.setTaskGraphId(graph.id);
25647
+ await this.builder.saveSession();
25648
+ return graph;
25649
+ }
25650
+ snapshot() {
25651
+ const s = this.builder.getSession();
25652
+ const spec = s.spec;
25653
+ return {
25654
+ sessionId: s.id,
25655
+ phase: s.phase,
25656
+ title: s.title,
25657
+ questionCount: s.questionCount,
25658
+ minQuestions: this.minQuestions,
25659
+ maxQuestions: this.maxQuestions,
25660
+ answers: s.answers.map((a) => ({ question: a.question, answer: a.answer })),
25661
+ spec: spec ? {
25662
+ id: spec.id,
25663
+ title: spec.title,
25664
+ overview: spec.overview,
25665
+ requirements: spec.requirements.map((r) => ({
25666
+ priority: r.priority,
25667
+ description: r.description
25668
+ }))
25669
+ } : void 0,
25670
+ graphId: s.taskGraphId,
25671
+ taskCount: this.graph ? this.graph.nodes.size : 0,
25672
+ board: this.graph ? buildBoardTasks(this.graph) : void 0,
25673
+ prompt: this.builder.getAIPrompt()
25674
+ };
25675
+ }
25676
+ // ── internals ────────────────────────────────────────────────────────────
25677
+ async persistSpec(spec) {
25678
+ try {
25679
+ await this.o.specStore.save(spec);
25680
+ } catch {
25681
+ }
25682
+ }
25683
+ async persistGraph(graph) {
25684
+ try {
25685
+ await this.o.graphStore.save(graph);
25686
+ } catch {
25687
+ }
25688
+ }
25689
+ /**
25690
+ * Port of the CLI `trySaveImplementationPlan` operating on this driver's
25691
+ * builder. Captures the prose plan that precedes the task JSON block.
25692
+ */
25693
+ trySaveImplementationPlan(text) {
25694
+ const current = this.builder.getSession().implementation ?? "";
25695
+ const jsonStart = text.match(/```json\s*\[/);
25696
+ if (jsonStart?.index && jsonStart.index > 0) {
25697
+ const plan = text.substring(0, jsonStart.index).trim();
25698
+ if (plan.length > 50 && plan !== current && !isExplanatoryText(plan)) {
25699
+ this.builder.setImplementation(plan);
25700
+ return true;
25701
+ }
25702
+ }
25703
+ if (text.length > 100 && !text.includes("```json") && text.trim() !== current && !isExplanatoryText(text)) {
25704
+ this.builder.setImplementation(text.trim());
25705
+ return true;
25706
+ }
25707
+ return false;
25708
+ }
25709
+ /**
25710
+ * Port of the CLI `trySaveTasksFromAIOutput`: parse a task JSON array from the
25711
+ * agent output, build (or extend) the tracker + graph, persist to disk, and
25712
+ * link the graphId to the session. Returns the graphId on success.
25713
+ */
25714
+ async tryBuildTasksFromOutput(text) {
25715
+ const json = this.builder.extractJSONArray(text);
25716
+ if (!json) return void 0;
25717
+ let tasks;
25718
+ try {
25719
+ tasks = JSON.parse(json);
25720
+ } catch {
25721
+ return void 0;
25722
+ }
25723
+ const valid = tasks.filter(
25724
+ (t) => t && typeof t === "object" && typeof t.title === "string" && t.title.length > 0
25725
+ );
25726
+ if (valid.length === 0) return void 0;
25727
+ const spec = this.builder.getSession().spec;
25728
+ if (!spec) return void 0;
25729
+ if (!this.tracker || !this.graph) {
25730
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
25731
+ this.graph = await tracker.createGraph(spec.id, spec.title);
25732
+ this.tracker = tracker;
25733
+ }
25734
+ const refMap = /* @__PURE__ */ new Map();
25735
+ const created = [];
25736
+ valid.forEach((task, i) => {
25737
+ const node = addTaskToTracker(this.tracker, task);
25738
+ created.push({ nodeId: node.id, task });
25739
+ if (typeof task.id === "string" && task.id.trim()) {
25740
+ refMap.set(task.id.trim().toLowerCase(), node.id);
25741
+ }
25742
+ refMap.set(`t${i + 1}`, node.id);
25743
+ refMap.set(String(i + 1), node.id);
25744
+ refMap.set(normalizeTaskRef(String(task.title)), node.id);
25745
+ });
25746
+ for (const { nodeId, task } of created) {
25747
+ const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
25748
+ for (const ref of deps) {
25749
+ const depId = refMap.get(normalizeTaskRef(String(ref)));
25750
+ if (depId && depId !== nodeId) this.tracker.addDependency(depId, nodeId);
25751
+ }
25752
+ }
25753
+ await this.persistGraph(this.graph);
25754
+ this.builder.setTaskGraphId(this.graph.id);
25755
+ await this.builder.saveSession();
25756
+ return this.graph.id;
25757
+ }
25758
+ };
25759
+ var TASK_TYPES2 = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
25760
+ var TASK_PRIORITIES = ["critical", "high", "medium", "low"];
25761
+ function normalizeTaskRef(ref) {
25762
+ return ref.trim().toLowerCase();
25763
+ }
25764
+ function addTaskToTracker(tracker, task) {
25765
+ return tracker.addNode({
25766
+ title: String(task.title),
25767
+ description: String(task.description ?? ""),
25768
+ type: TASK_TYPES2.includes(String(task.type)) ? String(task.type) : "feature",
25769
+ priority: TASK_PRIORITIES.includes(String(task.priority)) ? String(task.priority) : "medium",
25770
+ status: "pending",
25771
+ estimateHours: Number(task.estimateHours) || 2,
25772
+ tags: Array.isArray(task.tags) ? task.tags.map(String) : []
25773
+ });
25774
+ }
25775
+ function isExplanatoryText(text) {
25776
+ const lower = text.toLowerCase();
25777
+ 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(".");
25778
+ }
25779
+
25780
+ // src/sdd/start-sdd-run.ts
25781
+ function startSddRun(opts) {
25782
+ SddParallelRun.resetOrphans(opts.tracker);
25783
+ const run = new SddParallelRun({
25784
+ tracker: opts.tracker,
25785
+ graph: opts.graph,
25786
+ agent: opts.agent,
25787
+ projectRoot: opts.projectRoot,
25788
+ parallelSlots: opts.parallelSlots,
25789
+ taskTimeoutMs: opts.taskTimeoutMs,
25790
+ taskIdleTimeoutMs: opts.taskIdleTimeoutMs,
25791
+ maxFailedRetrySweeps: opts.maxFailedRetrySweeps,
25792
+ verifyTask: opts.verifyTask,
25793
+ conflictResolver: opts.conflictResolver,
25794
+ superviseFailure: opts.superviseFailure,
25795
+ subagentFactory: opts.subagentFactory,
25796
+ events: opts.events,
25797
+ worktrees: opts.worktrees,
25798
+ maxRecoveryRounds: opts.maxRecoveryRounds ?? 1,
25799
+ onProgress: opts.onProgress,
25800
+ defaultModel: opts.defaultModel,
25801
+ defaultProvider: opts.defaultProvider,
25802
+ fallbackModels: opts.fallbackModels
25803
+ });
25804
+ const projector = new SddBoardProjector({
25805
+ runId: run.runId,
25806
+ graph: opts.graph,
25807
+ tracker: opts.tracker,
25808
+ events: opts.events,
25809
+ store: opts.boardStore,
25810
+ specId: opts.graph.specId,
25811
+ defaultModel: opts.defaultModel,
25812
+ defaultProvider: opts.defaultProvider,
25813
+ fallbackModels: opts.fallbackModels
25814
+ });
25815
+ opts.registry?.register({
25816
+ runId: run.runId,
25817
+ specId: opts.graph.specId,
25818
+ pause: () => run.pause(),
25819
+ resume: () => run.resume(),
25820
+ stop: () => run.stop(),
25821
+ retryTask: (id) => run.retryTask(id),
25822
+ retryAllFailed: () => run.retryAllFailed(),
25823
+ reassignTask: (id, name) => run.reassignTask(id, name),
25824
+ setTaskModel: (id, model, provider) => run.setTaskModel(id, model, provider),
25825
+ setTaskFallbacks: (id, fb) => run.setTaskFallbacks(id, fb),
25826
+ setTaskVerification: (id, cmd) => run.setTaskVerification(id, cmd),
25827
+ cancelTask: (id) => run.cancelTask(id),
25828
+ deleteTask: (id) => run.deleteTask(id),
25829
+ splitTask: (id, subtasks) => run.splitTask(id, subtasks),
25830
+ cleanupWorktrees: () => run.cleanupWorktrees(),
25831
+ rollback: () => run.rollback(),
25832
+ getBaseBranch: () => run.getBaseBranch(),
25833
+ getMergedCommits: () => run.getMergedCommits(),
25834
+ snapshot: () => projector.snapshot(),
25835
+ isRunning: () => run.isRunning()
25836
+ });
25837
+ const drainMs = opts.controlDrainMs ?? 500;
25838
+ const controlTimer = setInterval(() => {
25839
+ void opts.boardStore.drainControl(run.runId).then((cmds) => {
25840
+ for (const c of cmds) {
25841
+ const p = c.payload ?? {};
25842
+ if (c.type === "pause") run.pause();
25843
+ else if (c.type === "resume") run.resume();
25844
+ else if (c.type === "stop") run.stop();
25845
+ else if (c.type === "retry" && p.taskId) run.retryTask(p.taskId);
25846
+ else if (c.type === "retry_all_failed") run.retryAllFailed();
25847
+ else if (c.type === "reassign" && p.taskId) run.reassignTask(p.taskId, p.agentName ?? "");
25848
+ else if (c.type === "set_task_model" && p.taskId) run.setTaskModel(p.taskId, p.model, p.provider);
25849
+ else if (c.type === "set_task_fallbacks" && p.taskId) run.setTaskFallbacks(p.taskId, p.fallbackModels);
25850
+ else if (c.type === "set_task_verification" && p.taskId)
25851
+ run.setTaskVerification(p.taskId, p.verificationCommand);
25852
+ else if (c.type === "cancel_task" && p.taskId) void run.cancelTask(p.taskId);
25853
+ else if (c.type === "delete_task" && p.taskId) run.deleteTask(p.taskId);
25854
+ else if (c.type === "split_task" && p.taskId && p.subtasks?.length) run.splitTask(p.taskId, p.subtasks);
25855
+ else if (c.type === "cleanup_worktrees") void run.cleanupWorktrees();
25856
+ else if (c.type === "rollback") void run.rollback();
25857
+ }
25858
+ });
25859
+ }, drainMs);
25860
+ controlTimer.unref?.();
25861
+ const completion = (async () => {
25862
+ try {
25863
+ return await run.run();
25864
+ } finally {
25865
+ clearInterval(controlTimer);
25866
+ await projector.drain().catch(() => {
25867
+ });
25868
+ projector.dispose();
25869
+ opts.registry?.clear(run.runId);
25870
+ }
25871
+ })();
25872
+ return {
25873
+ run,
25874
+ runId: run.runId,
25875
+ projector,
25876
+ completion,
25877
+ stop: () => run.stop()
25878
+ };
25879
+ }
25880
+ var MAX_SLUG = 40;
25881
+ var WorktreeManager = class {
25882
+ projectRoot;
25883
+ events;
25884
+ gitBin;
25885
+ runGit;
25886
+ /** Keyed by ownerId. */
25887
+ handles = /* @__PURE__ */ new Map();
25888
+ usedSlugs = /* @__PURE__ */ new Set();
25889
+ constructor(opts) {
25890
+ this.projectRoot = resolve(opts.projectRoot);
25891
+ this.events = opts.events;
25892
+ this.gitBin = opts.gitBin ?? "git";
25893
+ this.runGit = opts.run ?? ((args, cwd) => this.defaultRun(args, cwd));
25894
+ }
25895
+ /** Create a fresh worktree + branch forked from the current base branch. */
25896
+ async allocate(ownerId, opts = {}) {
25897
+ const existing = this.handles.get(ownerId);
25898
+ if (existing && (existing.status === "allocating" || existing.status === "active")) {
25899
+ return existing;
25900
+ }
25901
+ const slug = this.makeSlug(opts.slugHint ?? ownerId);
25902
+ const branch = `wstack/ap/${slug}`;
25903
+ const dir = join(this.worktreesRoot(), slug);
25904
+ const absDir = resolve(dir);
25905
+ const absRoot = resolve(this.projectRoot);
25906
+ if (!absDir.startsWith(absRoot + sep)) {
25907
+ throw new Error(`Worktree dir "${absDir}" resolves outside project root`);
25908
+ }
25909
+ const baseBranch = opts.baseBranch ?? await this.detectBaseBranch();
25910
+ const handle = {
25911
+ id: slug,
25912
+ ownerId,
25913
+ ownerLabel: opts.ownerLabel ?? opts.slugHint ?? ownerId,
25914
+ slug,
25915
+ dir,
25916
+ branch,
25917
+ baseBranch,
25918
+ status: "allocating",
25919
+ createdAt: Date.now(),
25920
+ updatedAt: Date.now(),
25921
+ insertions: 0,
25922
+ deletions: 0,
25923
+ files: 0
25924
+ };
25925
+ this.handles.set(ownerId, handle);
25926
+ try {
25927
+ await mkdir(this.worktreesRoot(), { recursive: true });
25928
+ const res = await this.runGit(
25929
+ ["worktree", "add", "-b", branch, dir, baseBranch],
25930
+ this.projectRoot
25931
+ );
25932
+ if (res.code !== 0) {
25933
+ return this.fail(handle, res.stderr || "git worktree add failed");
25934
+ }
25935
+ } catch (err) {
25936
+ return this.fail(handle, toErrorMessage(err));
25937
+ }
25938
+ this.setStatus(handle, "active");
25939
+ this.emit("worktree.allocated", {
25940
+ handleId: handle.id,
25941
+ ownerId: handle.ownerId,
25942
+ ownerLabel: handle.ownerLabel,
25943
+ slug: handle.slug,
25944
+ dir: handle.dir,
25945
+ branch: handle.branch,
25946
+ baseBranch: handle.baseBranch
25947
+ });
25948
+ return handle;
25949
+ }
25950
+ /** Stage everything and commit inside the worktree. */
25951
+ async commitAll(handle, message) {
25952
+ this.setStatus(handle, "committing");
25953
+ await this.runGit(["add", "-A"], handle.dir);
25954
+ const staged = await this.runGit(["diff", "--cached", "--quiet"], handle.dir);
25955
+ if (staged.code === 0) {
25956
+ this.emitCommitted(handle, false);
25957
+ return { committed: false };
25958
+ }
25959
+ const idArgs = await this.identityArgs(handle.dir);
25960
+ const committed = await this.runGit([...idArgs, "commit", "-m", message], handle.dir);
25961
+ if (committed.code !== 0) {
25962
+ this.fail(handle, committed.stderr || "git commit failed");
25963
+ return { committed: false };
25964
+ }
25965
+ const stats = await this.collectStats(handle.dir);
25966
+ handle.insertions = stats.insertions;
25967
+ handle.deletions = stats.deletions;
25968
+ handle.files = stats.files;
25969
+ handle.sha = stats.sha;
25970
+ handle.updatedAt = Date.now();
25971
+ this.emitCommitted(handle, true);
25972
+ return { committed: true };
25973
+ }
25974
+ /** Merge the worktree branch back into the base branch (squash by default). */
25975
+ async merge(handle, opts = {}) {
25976
+ const squash = opts.squash ?? true;
25977
+ this.setStatus(handle, "merging");
25978
+ const checkout = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
25979
+ if (checkout.code !== 0) {
25980
+ this.fail(handle, checkout.stderr || `checkout ${handle.baseBranch} failed`);
25981
+ return { ok: false, stderr: checkout.stderr };
25982
+ }
25983
+ const mergeArgs = squash ? ["merge", "--squash", handle.branch] : ["merge", "--no-ff", handle.branch];
25984
+ const merged = await this.runGit(mergeArgs, this.projectRoot);
25985
+ if (merged.code !== 0) {
25986
+ const fromOutput = parseConflictPaths(`${merged.stdout}
25987
+ ${merged.stderr}`);
25988
+ const fromIndex = await this.unmergedFiles();
25989
+ const conflictFiles = [.../* @__PURE__ */ new Set([...fromOutput, ...fromIndex])];
25990
+ if (opts.resolve) {
25991
+ const finalized = await this.tryResolveConflict(handle, conflictFiles, opts);
25992
+ if (finalized) return finalized;
25993
+ }
25994
+ await this.runGit(["reset", "--hard", "HEAD"], this.projectRoot);
25995
+ handle.conflictFiles = conflictFiles;
25996
+ this.setStatus(handle, "needs-review", { lastError: merged.stderr });
25997
+ this.emit("worktree.conflict", {
25998
+ handleId: handle.id,
25999
+ ownerId: handle.ownerId,
26000
+ branch: handle.branch,
26001
+ conflictFiles
26002
+ });
26003
+ return { ok: false, conflict: true, conflictFiles, stderr: merged.stderr };
26004
+ }
26005
+ if (squash) {
26006
+ const msg = opts.message ?? `merge ${handle.branch} (squash)`;
26007
+ const idArgs = await this.identityArgs(this.projectRoot);
26008
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
26009
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
26010
+ this.fail(handle, commit.stderr || "squash commit failed");
26011
+ return { ok: false, stderr: commit.stderr };
26012
+ }
26013
+ }
26014
+ this.setStatus(handle, "merged");
26015
+ this.emit("worktree.merged", {
26016
+ handleId: handle.id,
26017
+ ownerId: handle.ownerId,
26018
+ branch: handle.branch,
26019
+ baseBranch: handle.baseBranch,
26020
+ squash
26021
+ });
26022
+ return { ok: true };
26023
+ }
26024
+ /**
26025
+ * Current tip SHA of a handle's base branch (without checking it out). Capture
26026
+ * this before a merge so a regressed merge can be reverted to exactly this
26027
+ * commit — unambiguous even when a squash produced no diff. Returns null on
26028
+ * failure (caller then skips the revert).
26029
+ */
26030
+ async baseHead(handle) {
26031
+ const res = await this.runGit(["rev-parse", handle.baseBranch], this.projectRoot);
26032
+ const sha = res.stdout.trim();
26033
+ return res.code === 0 && sha ? sha : null;
26034
+ }
26035
+ /**
26036
+ * Hard-reset the base branch back to `sha` (a value previously returned by
26037
+ * {@link baseHead}). Used to undo a squash-merge whose integrated result failed
26038
+ * re-verification, so an auto-resolved-but-broken merge never sticks on base.
26039
+ * Safe because SDD merges are serialized — no other commit lands in between.
26040
+ */
26041
+ async revertBaseTo(handle, sha) {
26042
+ const co = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
26043
+ if (co.code !== 0) return false;
26044
+ const reset = await this.runGit(["reset", "--hard", sha], this.projectRoot);
26045
+ return reset.code === 0;
26046
+ }
26047
+ /**
26048
+ * Current base branch + tip SHA, captured WITHOUT a handle. The SDD run calls
26049
+ * this once at start so a later rollback knows which branch the run's squash
26050
+ * commits landed on. Returns null when not in a usable git state.
26051
+ */
26052
+ async currentBase() {
26053
+ const branch = await this.detectBaseBranch();
26054
+ const head = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
26055
+ const sha = head.stdout.trim();
26056
+ return head.code === 0 && sha ? { branch, sha } : null;
26057
+ }
26058
+ /**
26059
+ * Force-remove EVERY managed worktree + branch this project owns, without
26060
+ * relying on the in-memory `handles` map — so it works post-run (a fresh
26061
+ * manager can clean up a previous run's leftovers). Enumerates
26062
+ * `git worktree list --porcelain`, removes every checkout living under the
26063
+ * `.wrongstack/worktrees` root, deletes every `wstack/ap/*` branch, then prunes.
26064
+ * Returns the number of worktrees removed. Never throws — best-effort cleanup.
26065
+ */
26066
+ async cleanupAllManaged() {
26067
+ const root = resolve(this.worktreesRoot());
26068
+ let removed = 0;
26069
+ try {
26070
+ const listed = await this.runGit(["worktree", "list", "--porcelain"], this.projectRoot);
26071
+ for (const line of listed.stdout.split("\n")) {
26072
+ const m = line.match(/^worktree\s+(.+?)\s*$/);
26073
+ if (!m?.[1]) continue;
26074
+ const dir = resolve(m[1]);
26075
+ if (dir !== root && (dir === root || dir.startsWith(root + sep))) {
26076
+ const rm3 = await this.runGit(["worktree", "remove", "--force", dir], this.projectRoot);
26077
+ if (rm3.code === 0) removed++;
26078
+ }
26079
+ }
26080
+ } catch {
26081
+ }
26082
+ try {
26083
+ const branches = await this.runGit(
26084
+ ["branch", "--list", "--format=%(refname:short)", "wstack/ap/*"],
26085
+ this.projectRoot
26086
+ );
26087
+ for (const b of branches.stdout.split("\n").map((s) => s.trim()).filter(Boolean)) {
26088
+ await this.runGit(["branch", "-D", b], this.projectRoot);
26089
+ }
26090
+ } catch {
26091
+ }
26092
+ await this.runGit(["worktree", "prune"], this.projectRoot).catch(() => void 0);
26093
+ this.handles.clear();
26094
+ this.emit("worktree.released", {
26095
+ handleId: "cleanup-all",
26096
+ ownerId: "cleanup-all",
26097
+ branch: "wstack/ap/*",
26098
+ kept: false
26099
+ });
26100
+ return { removed };
26101
+ }
26102
+ /**
26103
+ * Undo a run's squash commits by reverting each (newest → oldest) on the base
26104
+ * branch — history-preserving, never a destructive reset. Refuses on a dirty
26105
+ * working tree (so uncommitted work is never clobbered) and aborts cleanly if a
26106
+ * revert conflicts, reporting which SHA. `shas` are the run commit SHAs in the
26107
+ * order they landed; this reverses them. Returns the count reverted.
26108
+ */
26109
+ async revertCommits(baseBranch, shas) {
26110
+ if (shas.length === 0) return { ok: true, reverted: 0, reason: "nothing to revert" };
26111
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
26112
+ if (status.stdout.trim().length > 0) {
26113
+ return { ok: false, reverted: 0, reason: "working tree has uncommitted changes \u2014 commit or stash first" };
26114
+ }
26115
+ const co = await this.runGit(["checkout", baseBranch], this.projectRoot);
26116
+ if (co.code !== 0) {
26117
+ return { ok: false, reverted: 0, reason: co.stderr || `checkout ${baseBranch} failed` };
26118
+ }
26119
+ const idArgs = await this.identityArgs(this.projectRoot);
26120
+ let reverted = 0;
26121
+ for (const sha of [...shas].reverse()) {
26122
+ const res = await this.runGit([...idArgs, "revert", "--no-edit", sha], this.projectRoot);
26123
+ if (res.code !== 0) {
26124
+ await this.runGit(["revert", "--abort"], this.projectRoot).catch(() => void 0);
26125
+ return {
26126
+ ok: false,
26127
+ reverted,
26128
+ reason: `revert of ${sha.slice(0, 8)} failed: ${(res.stderr || res.stdout).trim().split("\n")[0] ?? "conflict"}`
26129
+ };
26130
+ }
26131
+ reverted++;
26132
+ }
26133
+ return { ok: true, reverted };
26134
+ }
26135
+ /**
26136
+ * Run the caller-supplied resolver against a conflicted squash-merge, then
26137
+ * commit if it cleared every marker. Returns a successful `MergeResult` on a
26138
+ * clean resolution, or `null` to signal the caller should fall back to the
26139
+ * abort path. Never leaves the base tree committed-but-dirty: a partial or
26140
+ * failed resolution returns `null` and the caller hard-resets.
26141
+ */
26142
+ async tryResolveConflict(handle, conflictFiles, opts) {
26143
+ let resolved = false;
26144
+ try {
26145
+ resolved = opts.resolve ? await opts.resolve({ conflictFiles, cwd: this.projectRoot }) : false;
26146
+ } catch {
26147
+ resolved = false;
26148
+ }
26149
+ if (!resolved) return null;
26150
+ await this.runGit(["add", "-A"], this.projectRoot);
26151
+ if (await this.hasConflictMarkers()) return null;
26152
+ const idArgs = await this.identityArgs(this.projectRoot);
26153
+ const msg = opts.message ?? `merge ${handle.branch} (squash, conflict resolved)`;
26154
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
26155
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
26156
+ return null;
26157
+ }
26158
+ handle.conflictFiles = conflictFiles;
26159
+ this.setStatus(handle, "merged");
26160
+ this.emit("worktree.merged", {
26161
+ handleId: handle.id,
26162
+ ownerId: handle.ownerId,
26163
+ branch: handle.branch,
26164
+ baseBranch: handle.baseBranch,
26165
+ squash: true
26166
+ });
26167
+ return { ok: true, resolved: true, conflictFiles };
26168
+ }
26169
+ /**
26170
+ * True when staged content still carries conflict markers. `git diff --cached
26171
+ * --check` exits nonzero and prints a "leftover conflict marker" line for each
26172
+ * survivor; whitespace-only errors (also flagged by --check) are ignored so a
26173
+ * clean resolution with unrelated whitespace is not rejected.
26174
+ */
26175
+ async hasConflictMarkers() {
26176
+ const check = await this.runGit(["diff", "--cached", "--check"], this.projectRoot);
26177
+ if (check.code === 0) return false;
26178
+ return /conflict marker/i.test(`${check.stdout}
26179
+ ${check.stderr}`);
26180
+ }
26181
+ /**
26182
+ * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`)
26183
+ * are left on disk for inspection.
26184
+ */
26185
+ async release(handle, opts = {}) {
26186
+ const keep = opts.keep || handle.status === "needs-review" || handle.status === "failed";
26187
+ if (!keep) {
26188
+ await this.runGit(["worktree", "remove", "--force", handle.dir], this.projectRoot);
26189
+ await this.runGit(["branch", "-D", handle.branch], this.projectRoot);
26190
+ await this.runGit(["worktree", "prune"], this.projectRoot);
26191
+ this.handles.delete(handle.ownerId);
26192
+ }
26193
+ this.emit("worktree.released", {
26194
+ handleId: handle.id,
26195
+ ownerId: handle.ownerId,
26196
+ branch: handle.branch,
26197
+ kept: keep
26198
+ });
26199
+ }
26200
+ get(ownerId) {
26201
+ return this.handles.get(ownerId);
26202
+ }
26203
+ list() {
26204
+ return [...this.handles.values()];
26205
+ }
26206
+ // ── internals ────────────────────────────────────────────────────────────
26207
+ worktreesRoot() {
26208
+ return join(this.projectRoot, ".wrongstack", "worktrees");
26209
+ }
26210
+ async detectBaseBranch() {
26211
+ const head = await this.runGit(["rev-parse", "--abbrev-ref", "HEAD"], this.projectRoot);
26212
+ const name = head.stdout.trim();
26213
+ if (name && name !== "HEAD") return name;
26214
+ const sha = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
26215
+ return sha.stdout.trim() || "HEAD";
26216
+ }
26217
+ makeSlug(hint) {
26218
+ let base = hint.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+/, "").replace(/[-.]+$/, "").slice(0, MAX_SLUG).replace(/[-.]+$/, "");
26219
+ if (!base) base = "wt";
26220
+ let slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
26221
+ while (this.usedSlugs.has(slug)) slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
26222
+ this.usedSlugs.add(slug);
26223
+ return slug;
26224
+ }
26225
+ async collectStats(dir) {
26226
+ const sha = (await this.runGit(["rev-parse", "HEAD"], dir)).stdout.trim();
26227
+ const numstat = await this.runGit(["show", "--numstat", "--format=", "HEAD"], dir);
26228
+ let insertions = 0;
26229
+ let deletions = 0;
26230
+ let files = 0;
26231
+ for (const line of numstat.stdout.split("\n")) {
26232
+ const m = line.trim().match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
26233
+ if (!m) continue;
26234
+ files++;
26235
+ if (m[1] !== "-") insertions += Number(m[1]);
26236
+ if (m[2] !== "-") deletions += Number(m[2]);
26237
+ }
26238
+ return { insertions, deletions, files, sha };
26239
+ }
26240
+ /**
26241
+ * `git -c user.*` fallback so commits succeed on machines and CI runners
26242
+ * that have no global git identity configured. Returns `[]` when both
26243
+ * `user.name` and `user.email` are already set (the common case), so a real
26244
+ * user's identity is never overridden. The worktree branch commits are
26245
+ * squashed away on merge, so the fallback identity never reaches the base
26246
+ * branch history.
26247
+ */
26248
+ async identityArgs(cwd) {
26249
+ const name = (await this.runGit(["config", "user.name"], cwd)).stdout.trim();
26250
+ const email = (await this.runGit(["config", "user.email"], cwd)).stdout.trim();
26251
+ if (name && email) return [];
26252
+ return [
26253
+ "-c",
26254
+ `user.name=${name || "AutoPhase"}`,
26255
+ "-c",
26256
+ `user.email=${email || "autophase@agent.local"}`
26257
+ ];
26258
+ }
26259
+ async unmergedFiles() {
26260
+ const res = await this.runGit(["diff", "--name-only", "--diff-filter=U"], this.projectRoot);
26261
+ return res.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
26262
+ }
26263
+ emitCommitted(handle, committed) {
26264
+ this.emit("worktree.committed", {
26265
+ handleId: handle.id,
26266
+ ownerId: handle.ownerId,
26267
+ branch: handle.branch,
26268
+ committed,
26269
+ insertions: handle.insertions,
26270
+ deletions: handle.deletions,
26271
+ files: handle.files,
26272
+ sha: handle.sha
26273
+ });
26274
+ }
26275
+ fail(handle, error) {
26276
+ this.setStatus(handle, "failed", { lastError: error });
26277
+ this.emit("worktree.failed", {
26278
+ handleId: handle.id,
26279
+ ownerId: handle.ownerId,
26280
+ branch: handle.branch,
26281
+ error
26282
+ });
26283
+ return handle;
26284
+ }
26285
+ setStatus(handle, status, patch) {
26286
+ handle.status = status;
26287
+ handle.updatedAt = Date.now();
26288
+ if (patch) Object.assign(handle, patch);
26289
+ }
26290
+ emit(event, payload) {
26291
+ this.events?.emit(event, payload);
26292
+ }
26293
+ defaultRun(args, cwd) {
26294
+ return new Promise((res) => {
26295
+ let stdout = "";
26296
+ let stderr = "";
26297
+ const MAX_GIT_OUTPUT = 1e6;
26298
+ const child = spawn(this.gitBin, args, {
26299
+ cwd,
26300
+ env: buildChildEnv(),
26301
+ stdio: ["ignore", "pipe", "pipe"],
26302
+ signal: AbortSignal.timeout(3e4),
26303
+ windowsHide: true
26304
+ });
26305
+ child.stdout?.on("data", (c) => {
26306
+ if (stdout.length < MAX_GIT_OUTPUT) stdout += c.toString();
26307
+ });
26308
+ child.stderr?.on("data", (c) => {
26309
+ if (stderr.length < MAX_GIT_OUTPUT) stderr += c.toString();
26310
+ });
26311
+ child.on("error", (err) => res({ code: 1, stdout, stderr: err.message }));
26312
+ child.on("close", (code) => res({ code: code ?? 1, stdout, stderr }));
26313
+ });
26314
+ }
26315
+ };
26316
+ function parseConflictPaths(output) {
26317
+ const paths = /* @__PURE__ */ new Set();
26318
+ for (const line of output.split("\n")) {
26319
+ const m = line.match(/^CONFLICT \([^)]*\): Merge conflict in (.+?)\s*$/);
26320
+ if (m?.[1]) paths.add(m[1]);
26321
+ }
26322
+ return [...paths];
26323
+ }
26324
+
26325
+ // src/sdd/sdd-lifecycle.ts
26326
+ async function cleanupSddWorktrees(projectRoot) {
26327
+ const wt = new WorktreeManager({ projectRoot });
26328
+ return wt.cleanupAllManaged();
26329
+ }
26330
+ async function rollbackSddRunFromDisk(opts) {
26331
+ const store = new SddBoardStore({ baseDir: opts.boardsDir });
26332
+ const runId = opts.runId ?? (await store.list())[0]?.runId;
26333
+ if (!runId) return { ok: false, reverted: 0, reason: "no SDD board found to roll back" };
26334
+ const snap = await store.load(runId);
26335
+ if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
26336
+ if (!snap.baseBranch) {
26337
+ return { ok: false, reverted: 0, reason: "this run did not record a base branch (no worktree run)" };
26338
+ }
26339
+ const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
26340
+ if (shas.length === 0) {
26341
+ return { ok: false, reverted: 0, reason: "no merged commits recorded for this run" };
26342
+ }
26343
+ const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
26344
+ return wt.revertCommits(snap.baseBranch, shas);
26345
+ }
26346
+ async function destroySddProject(opts) {
26347
+ const { removed } = await cleanupSddWorktrees(opts.projectRoot).catch(() => ({ removed: 0 }));
26348
+ const deleted = [];
26349
+ const rmDir = async (dir, label) => {
26350
+ try {
26351
+ await fsp2.rm(dir, { recursive: true, force: true });
26352
+ deleted.push(label);
26353
+ } catch {
26354
+ }
26355
+ };
26356
+ const rmFile = async (file, label) => {
26357
+ try {
26358
+ await fsp2.unlink(file);
26359
+ deleted.push(label);
26360
+ } catch {
26361
+ }
26362
+ };
26363
+ await rmFile(opts.paths.projectSddSession, "session");
26364
+ await rmDir(opts.paths.projectSpecs, "specs");
26365
+ await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
26366
+ await rmDir(opts.paths.projectSddBoards, "boards");
26367
+ return { worktreesRemoved: removed, deleted };
26368
+ }
26369
+
23763
26370
  // src/observability/metrics.ts
23764
26371
  var RESERVOIR_SIZE = 1024;
23765
26372
  function labelKey(labels) {
@@ -23919,9 +26526,9 @@ var DefaultHealthRegistry = class {
23919
26526
  }
23920
26527
  async runOne(check) {
23921
26528
  let timer = null;
23922
- const timeout = new Promise((resolve7) => {
26529
+ const timeout = new Promise((resolve8) => {
23923
26530
  timer = setTimeout(
23924
- () => resolve7({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
26531
+ () => resolve8({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
23925
26532
  this.timeoutMs
23926
26533
  );
23927
26534
  });
@@ -24104,7 +26711,7 @@ async function startMetricsServer(opts) {
24104
26711
  const tls = opts.tls;
24105
26712
  const useHttps = !!(tls?.cert && tls?.key);
24106
26713
  const host = opts.host ?? "127.0.0.1";
24107
- const path22 = opts.path ?? "/metrics";
26714
+ const path23 = opts.path ?? "/metrics";
24108
26715
  const healthPath = opts.healthPath ?? "/healthz";
24109
26716
  const healthRegistry = opts.healthRegistry;
24110
26717
  const listener = (req, res) => {
@@ -24114,7 +26721,7 @@ async function startMetricsServer(opts) {
24114
26721
  return;
24115
26722
  }
24116
26723
  const url = req.url.split("?")[0];
24117
- if (url === path22) {
26724
+ if (url === path23) {
24118
26725
  let body;
24119
26726
  try {
24120
26727
  body = renderPrometheus(opts.sink.snapshot());
@@ -24160,14 +26767,14 @@ async function startMetricsServer(opts) {
24160
26767
  const { createServer } = await import('http');
24161
26768
  server = createServer(listener);
24162
26769
  }
24163
- await new Promise((resolve7, reject) => {
26770
+ await new Promise((resolve8, reject) => {
24164
26771
  const onError = (err) => {
24165
26772
  server.off("listening", onListening);
24166
26773
  reject(err);
24167
26774
  };
24168
26775
  const onListening = () => {
24169
26776
  server.off("error", onError);
24170
- resolve7();
26777
+ resolve8();
24171
26778
  };
24172
26779
  server.once("error", onError);
24173
26780
  server.once("listening", onListening);
@@ -24178,9 +26785,9 @@ async function startMetricsServer(opts) {
24178
26785
  const protocol = useHttps ? "https" : "http";
24179
26786
  return {
24180
26787
  port: boundPort,
24181
- url: `${protocol}://${host}:${boundPort}${path22}`,
24182
- close: () => new Promise((resolve7, reject) => {
24183
- server.close((err) => err ? reject(err) : resolve7());
26788
+ url: `${protocol}://${host}:${boundPort}${path23}`,
26789
+ close: () => new Promise((resolve8, reject) => {
26790
+ server.close((err) => err ? reject(err) : resolve8());
24184
26791
  })
24185
26792
  };
24186
26793
  }
@@ -24852,6 +27459,6 @@ var allServers = () => ({
24852
27459
  ssh: { ...sshManagerServer(), enabled: false }
24853
27460
  });
24854
27461
 
24855
- export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, describeCatalogModel, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, playwrightServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveProviderModelList, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, sshManagerServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
27462
+ export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddBoardProjector, SddBoardStore, SddInterviewDriver, SddParallelRun, SddRunRegistry, SddSupervisor, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBoardSnapshot, buildBoardTasks, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, cleanupSddWorktrees, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, describeCatalogModel, destroySddProject, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, extractVerificationCommand, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, hasConflictMarkers, isExplanatoryText, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeCommandVerifier, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeLlmConflictResolver, makeLlmSubtaskGenerator, makePreferSideConflictResolver, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, playwrightServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveConflictText, resolveProviderModelList, resolveSessionLoggingConfig, rewriteConfigEncrypted, rollbackSddRunFromDisk, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, shortIdMap, slackServer, sshManagerServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, startSddRun, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
24856
27463
  //# sourceMappingURL=index.js.map
24857
27464
  //# sourceMappingURL=index.js.map