@wrongstack/core 0.272.2 → 0.273.1

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 (71) hide show
  1. package/dist/{agent-bridge-DFQYEeXf.d.ts → agent-bridge-DpKIxHhE.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-BZa_IEcd.d.ts → agent-subagent-runner-Dx7fZ1bE.d.ts} +14 -7
  3. package/dist/{brain-etbcbRwV.d.ts → brain-BDcQaku-.d.ts} +112 -5
  4. package/dist/{compactor-72ug-ZRB.d.ts → compactor-BuSdj3fq.d.ts} +1 -1
  5. package/dist/{config-rRS8yorV.d.ts → config-CR2yoG8c.d.ts} +61 -4
  6. package/dist/{context-Dw55zZ_Q.d.ts → context-DulAr8Zo.d.ts} +24 -0
  7. package/dist/coordination/index.d.ts +23 -16
  8. package/dist/coordination/index.js +192 -38
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-B0cj-Hry.d.ts → default-config-BbX4ojZs.d.ts} +1 -0
  11. package/dist/defaults/index.d.ts +29 -28
  12. package/dist/defaults/index.js +3238 -234
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +83 -3
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +1 -1
  18. package/dist/extension/index.d.ts +6 -6
  19. package/dist/{global-mailbox-DJ4EoRr0.d.ts → global-mailbox-CwcubDkA.d.ts} +1 -1
  20. package/dist/{goal-preamble-hM8BH7TK.d.ts → goal-preamble-Bu0a2uCG.d.ts} +10 -10
  21. package/dist/{goal-store-CWlbT0TO.d.ts → goal-store-CTmFuZ8J.d.ts} +1 -1
  22. package/dist/hq/index.d.ts +5 -5
  23. package/dist/hq/index.js +1 -0
  24. package/dist/hq/index.js.map +1 -1
  25. package/dist/{index-DWm_PE9L.d.ts → index-CTq5wU3m.d.ts} +14 -6
  26. package/dist/{index-2Lhk5v0o.d.ts → index-CxP-HBhX.d.ts} +8 -2
  27. package/dist/index.d.ts +230 -48
  28. package/dist/index.js +3731 -648
  29. package/dist/index.js.map +1 -1
  30. package/dist/infrastructure/index.d.ts +6 -6
  31. package/dist/kernel/index.d.ts +94 -14
  32. package/dist/kernel/index.js.map +1 -1
  33. package/dist/{mcp-servers-BpWHTKlE.d.ts → mcp-servers-BQaOE71z.d.ts} +3 -3
  34. package/dist/models/index.d.ts +5 -5
  35. package/dist/{models-registry-CXQFUn5t.d.ts → models-registry-BEcny4kP.d.ts} +1 -1
  36. package/dist/{multi-agent-coordinator-jyimfo7D.d.ts → multi-agent-coordinator-Bx8EFkv2.d.ts} +1 -1
  37. package/dist/{null-fleet-bus-DOGQcvrY.d.ts → null-fleet-bus-BC5ZXCQw.d.ts} +16 -6
  38. package/dist/observability/index.d.ts +2 -2
  39. package/dist/{parallel-eternal-engine-rItJBYp9.d.ts → parallel-eternal-engine-C345TI3n.d.ts} +10 -9
  40. package/dist/{path-resolver-DrpF5MGK.d.ts → path-resolver-C-W_wzkF.d.ts} +3 -3
  41. package/dist/{permission-CC7XFYWG.d.ts → permission-CsBGZkxp.d.ts} +1 -1
  42. package/dist/{permission-policy-cYR4RJmw.d.ts → permission-policy-g3Sg0GdZ.d.ts} +2 -2
  43. package/dist/{pipeline-Ckkn3AOA.d.ts → pipeline-xnw_24Z8.d.ts} +2 -2
  44. package/dist/{plan-templates-BvHw5Znw.d.ts → plan-templates-DGaiYEcS.d.ts} +32 -6
  45. package/dist/{provider-model-resolve-nZqnCeaR.d.ts → provider-model-resolve-Cz6OlIOp.d.ts} +3 -3
  46. package/dist/{provider-runner-zVOn1p67.d.ts → provider-runner-7J0HqF6B.d.ts} +3 -3
  47. package/dist/{retry-policy-BV7nzeAd.d.ts → retry-policy-kqXJOVkX.d.ts} +1 -1
  48. package/dist/sdd/index.d.ts +1114 -14
  49. package/dist/sdd/index.js +5516 -2949
  50. package/dist/sdd/index.js.map +1 -1
  51. package/dist/{secret-vault-eMBKfheR.d.ts → secret-vault-CMQUr-eB.d.ts} +1 -1
  52. package/dist/security/index.d.ts +5 -5
  53. package/dist/security/index.js +6 -0
  54. package/dist/security/index.js.map +1 -1
  55. package/dist/{selector-C4ORTOid.d.ts → selector-B4r34PWR.d.ts} +1 -1
  56. package/dist/{session-event-bridge-CeNpUL9w.d.ts → session-event-bridge-BD3LoyLC.d.ts} +1 -1
  57. package/dist/{session-reader-BepLSnGL.d.ts → session-reader-DjrKGD9c.d.ts} +1 -1
  58. package/dist/storage/index.d.ts +12 -12
  59. package/dist/storage/index.js +380 -13
  60. package/dist/storage/index.js.map +1 -1
  61. package/dist/tools/index.d.ts +2 -2
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/types/index.d.ts +20 -20
  64. package/dist/types/index.js +31 -0
  65. package/dist/types/index.js.map +1 -1
  66. package/dist/utils/index.d.ts +30 -4
  67. package/dist/utils/index.js +110 -1
  68. package/dist/utils/index.js.map +1 -1
  69. package/dist/{index-DqW4o62H.d.ts → worktree-manager-DHdrWQ_7.d.ts} +48 -90
  70. package/dist/{wstack-paths-hOpNLmvf.d.ts → wstack-paths-BqkDAkoh.d.ts} +2 -0
  71. 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
  };
@@ -2397,6 +2519,91 @@ var DefaultSessionStore = class _DefaultSessionStore {
2397
2519
  }
2398
2520
  }
2399
2521
  }
2522
+ /**
2523
+ * Streaming search over a session's JSONL. Walks the file once, parses
2524
+ * each event lazily, and yields only the events that match `predicate`.
2525
+ * Stops as soon as `opts.limit` matches are collected.
2526
+ *
2527
+ * Why this exists: `load()` parses the entire file into memory and
2528
+ * rebuilds `messages`/`toolCallEnds` for every caller. `search()` only
2529
+ * needs to know which events contain matching text — a per-line
2530
+ * predicate is enough. The full parse work (and the `_loadCache` poll)
2531
+ * is wasted in that case.
2532
+ *
2533
+ * Memory: O(hits) regardless of file size. Disk: one linear scan,
2534
+ * terminated at `limit` if the caller asked for one.
2535
+ *
2536
+ * Errors: missing file yields []. Corrupt lines are skipped (same
2537
+ * policy as `load()`). Aborting via `signal` rejects with `AbortError`.
2538
+ */
2539
+ async searchEvents(id, predicate, opts) {
2540
+ const file = this.sessionPath(id, ".jsonl");
2541
+ const limit = opts?.limit;
2542
+ const signal = opts?.signal;
2543
+ const out = [];
2544
+ let stat6;
2545
+ try {
2546
+ stat6 = await fsp2.stat(file);
2547
+ } catch (err) {
2548
+ if (err.code === "ENOENT") return [];
2549
+ throw err;
2550
+ }
2551
+ if (stat6.size === 0) return [];
2552
+ let fh;
2553
+ try {
2554
+ fh = await fsp2.open(file, "r");
2555
+ const CHUNK = 64 * 1024;
2556
+ const buf = Buffer.alloc(CHUNK);
2557
+ let leftover = "";
2558
+ let eventIndex = 0;
2559
+ for (let position = 0; ; position += buf.byteLength) {
2560
+ if (signal?.aborted) {
2561
+ const reason = signal.reason ?? new DOMException("Aborted", "AbortError");
2562
+ throw reason;
2563
+ }
2564
+ const { bytesRead } = await fh.read(buf, 0, CHUNK, position);
2565
+ if (bytesRead === 0) break;
2566
+ const text = leftover + buf.subarray(0, bytesRead).toString("utf8");
2567
+ const parts = text.split("\n");
2568
+ leftover = parts.pop() ?? "";
2569
+ for (const line of parts) {
2570
+ if (!line) continue;
2571
+ let ev;
2572
+ try {
2573
+ const parsed = JSON.parse(line);
2574
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.type !== "string" || typeof parsed.ts !== "string") {
2575
+ continue;
2576
+ }
2577
+ ev = parsed;
2578
+ } catch {
2579
+ continue;
2580
+ }
2581
+ if (predicate(ev, eventIndex, ev.ts)) {
2582
+ out.push({ event: ev, eventIndex, ts: ev.ts });
2583
+ if (limit !== void 0 && out.length >= limit) {
2584
+ return out;
2585
+ }
2586
+ }
2587
+ eventIndex++;
2588
+ }
2589
+ }
2590
+ if (leftover.trim()) {
2591
+ try {
2592
+ const parsed = JSON.parse(leftover);
2593
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
2594
+ const ev = parsed;
2595
+ if (predicate(ev, eventIndex, ev.ts)) {
2596
+ out.push({ event: ev, eventIndex, ts: ev.ts });
2597
+ }
2598
+ }
2599
+ } catch {
2600
+ }
2601
+ }
2602
+ return out;
2603
+ } finally {
2604
+ if (fh) await fh.close().catch(() => void 0);
2605
+ }
2606
+ }
2400
2607
  async list(limit = 20) {
2401
2608
  try {
2402
2609
  await ensureDir(this.dir);
@@ -3429,7 +3636,6 @@ var FileSessionWriter = class _FileSessionWriter {
3429
3636
  }
3430
3637
  const writeFd = await fsp2.open(tmpPath, "w", 384);
3431
3638
  try {
3432
- let copied = 0;
3433
3639
  let readOffset = 0;
3434
3640
  while (readOffset < newlineAfterCheckpoint) {
3435
3641
  const toCopy = Math.min(CHUNK_SIZE, newlineAfterCheckpoint - readOffset);
@@ -3438,7 +3644,6 @@ var FileSessionWriter = class _FileSessionWriter {
3438
3644
  if (r === 0) break;
3439
3645
  await writeFd.write(copyBuf, 0, r);
3440
3646
  readOffset += r;
3441
- copied += r;
3442
3647
  }
3443
3648
  const raw = await fsp2.readFile(this.filePath);
3444
3649
  const tail = raw.subarray(newlineAfterCheckpoint).toString("utf8");
@@ -4647,9 +4852,9 @@ ${body.trim()}`);
4647
4852
  if (!this.persistBackup || scope === "project-agents") return;
4648
4853
  try {
4649
4854
  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");
4855
+ const { writeFile: writeFile7, mkdir: mkdir8 } = await import('fs/promises');
4856
+ await mkdir8(this.backupDir, { recursive: true });
4857
+ await writeFile7(`${this.backupDir}/${scope}.md`, content, "utf8");
4653
4858
  } catch {
4654
4859
  }
4655
4860
  }
@@ -5390,6 +5595,9 @@ function isContextWindowModeId(id) {
5390
5595
  return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
5391
5596
  }
5392
5597
 
5598
+ // src/types/config.ts
5599
+ var DEFAULT_TUI_THINKING_WORD = "thinking";
5600
+
5393
5601
  // src/types/default-config.ts
5394
5602
  var DEFAULT_TOOLS_CONFIG = Object.freeze({
5395
5603
  defaultExecutionStrategy: "smart",
@@ -5397,6 +5605,7 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
5397
5605
  iterationTimeoutMs: 3e5,
5398
5606
  sessionTimeoutMs: 18e5,
5399
5607
  perIterationOutputCapBytes: 1e5,
5608
+ descriptionMode: Object.freeze({}),
5400
5609
  autoExtendLimit: true,
5401
5610
  restrictToProjectRoot: false
5402
5611
  });
@@ -5407,6 +5616,10 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
5407
5616
  var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
5408
5617
  autoProceedDelayMs: 45e3
5409
5618
  });
5619
+ var DEFAULT_CIRCUIT_BREAKER_CONFIG = Object.freeze({
5620
+ enabled: false,
5621
+ autoKillResetMs: 6e4
5622
+ });
5410
5623
  var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
5411
5624
  auditLevel: "standard",
5412
5625
  sampling: {
@@ -5433,7 +5646,8 @@ var BEHAVIOR_DEFAULTS = {
5433
5646
  hardThreshold: 0.9,
5434
5647
  autoCompact: true,
5435
5648
  preserveK: DEFAULT_CONTEXT_CONFIG.preserveK,
5436
- eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold
5649
+ eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold,
5650
+ strategy: "hybrid"
5437
5651
  },
5438
5652
  tools: {
5439
5653
  defaultExecutionStrategy: DEFAULT_TOOLS_CONFIG.defaultExecutionStrategy,
@@ -5441,6 +5655,7 @@ var BEHAVIOR_DEFAULTS = {
5441
5655
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
5442
5656
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
5443
5657
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
5658
+ descriptionMode: DEFAULT_TOOLS_CONFIG.descriptionMode,
5444
5659
  autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
5445
5660
  restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
5446
5661
  },
@@ -5455,6 +5670,13 @@ var BEHAVIOR_DEFAULTS = {
5455
5670
  allowOutsideProjectRoot: true
5456
5671
  },
5457
5672
  mcpServers: {},
5673
+ fallbackAuto: true,
5674
+ maxConcurrent: 4,
5675
+ yolo: false,
5676
+ nextPrediction: false,
5677
+ hints: true,
5678
+ debugStream: false,
5679
+ configScope: "global",
5458
5680
  indexing: {
5459
5681
  onSessionStart: true,
5460
5682
  onEdit: true,
@@ -5462,8 +5684,55 @@ var BEHAVIOR_DEFAULTS = {
5462
5684
  debounceMs: 400
5463
5685
  },
5464
5686
  session: { ...DEFAULT_SESSION_LOGGING_CONFIG },
5465
- autonomy: { autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs }
5687
+ autonomy: {
5688
+ defaultMode: "off",
5689
+ autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs,
5690
+ autoProceedMaxIterations: 50,
5691
+ autonomyNextPrompt: "auto {{suggestion}}",
5692
+ terminalTitleAnimation: true,
5693
+ yolo: false,
5694
+ streamFleet: true,
5695
+ chime: false,
5696
+ confirmExit: true,
5697
+ mouseMode: false,
5698
+ enhance: true,
5699
+ enhanceDelayMs: 6e4,
5700
+ enhanceLanguage: "original",
5701
+ statuslineMode: "detailed",
5702
+ thinkingWord: DEFAULT_TUI_THINKING_WORD
5703
+ },
5704
+ circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
5705
+ modelRuntime: {
5706
+ reasoning: { mode: "auto", effort: "high", preserve: false },
5707
+ cache: {}
5708
+ }
5466
5709
  };
5710
+ function isPlainRecord(value) {
5711
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5712
+ }
5713
+ function cloneJsonValue(value) {
5714
+ return structuredClone(value);
5715
+ }
5716
+ function fillMissingDefaults(target, defaults) {
5717
+ const value = cloneJsonValue(target);
5718
+ const changed = fillMissingDefaultsInPlace(value, defaults);
5719
+ return { value, changed };
5720
+ }
5721
+ function fillMissingDefaultsInPlace(target, defaults) {
5722
+ let changed = false;
5723
+ for (const [key, defaultValue] of Object.entries(defaults)) {
5724
+ if (!Object.prototype.hasOwnProperty.call(target, key)) {
5725
+ target[key] = cloneJsonValue(defaultValue);
5726
+ changed = true;
5727
+ continue;
5728
+ }
5729
+ const current = target[key];
5730
+ if (isPlainRecord(current) && isPlainRecord(defaultValue)) {
5731
+ changed = fillMissingDefaultsInPlace(current, defaultValue) || changed;
5732
+ }
5733
+ }
5734
+ return changed;
5735
+ }
5467
5736
  function envBool(v) {
5468
5737
  return !/^(0|false|no|off)$/i.test(v.trim());
5469
5738
  }
@@ -5515,27 +5784,139 @@ var defaultIndexing = {
5515
5784
  watchExternal: true,
5516
5785
  debounceMs: 400
5517
5786
  };
5518
- var IN_PROJECT_FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
5787
+ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
5788
+ "version",
5789
+ "model",
5790
+ "cwd",
5791
+ "context",
5792
+ "tools",
5793
+ "features",
5794
+ "autonomy",
5795
+ "indexing",
5796
+ "session",
5797
+ "log",
5798
+ "launch",
5799
+ "nextPrediction",
5800
+ "hints",
5801
+ "debugStream",
5802
+ "configScope",
5803
+ "maxConcurrent",
5804
+ "fallbackModels",
5805
+ "fallbackAuto",
5806
+ "models",
5807
+ "modelMatrix",
5808
+ "circuitBreaker",
5809
+ "adaptiveConcurrency",
5810
+ "modelRuntime"
5811
+ ]);
5812
+ var KNOWN_DENIED_IN_PROJECT = [
5813
+ { key: "provider", reason: "Provider id override; can intercept prompts/responses." },
5814
+ { key: "apiKey", reason: "Overrides user API key; exfiltrates prompts." },
5815
+ { key: "baseUrl", reason: "Redirects provider endpoint; leaks real API key." },
5816
+ { key: "providers", reason: "Per-provider apiKey/baseUrl/oauthConfig; same redirect/exfil." },
5817
+ { key: "mcpServers", reason: "Arbitrary command/args/env spawned at boot (RCE)." },
5818
+ { key: "hooks", reason: "Shell command arrays on lifecycle events (RCE)." },
5819
+ { key: "plugins", reason: "Dynamic npm package load at boot (RCE)." },
5820
+ { key: "sync", reason: "Carries githubToken credential and target repo." },
5821
+ { key: "yolo", reason: "Disables all permission confirmation prompts." },
5822
+ { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
5823
+ { key: "hq", reason: "Carries HQ client token credential and endpoint URL." }
5824
+ ];
5825
+ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
5826
+ "version",
5519
5827
  "provider",
5828
+ "model",
5520
5829
  "apiKey",
5521
5830
  "baseUrl",
5831
+ "maxConcurrent",
5522
5832
  "providers",
5833
+ "models",
5834
+ "modelMatrix",
5835
+ "context",
5836
+ "tools",
5523
5837
  "mcpServers",
5838
+ "fallbackModels",
5839
+ "fallbackAuto",
5524
5840
  "hooks",
5525
5841
  "plugins",
5526
- "sync",
5842
+ "log",
5843
+ "features",
5527
5844
  "yolo",
5845
+ "nextPrediction",
5846
+ "cwd",
5847
+ "autonomy",
5848
+ "hints",
5849
+ "debugStream",
5850
+ "configScope",
5851
+ "indexing",
5852
+ "circuitBreaker",
5853
+ "adaptiveConcurrency",
5854
+ "launch",
5855
+ "session",
5856
+ "modelRuntime",
5857
+ "hq",
5858
+ "sync",
5528
5859
  "extensions"
5529
5860
  ]);
5861
+ function assertInProjectAllowListComplete() {
5862
+ const missingFromBoth = [];
5863
+ for (const key of KNOWN_CONFIG_TOP_LEVEL_KEYS) {
5864
+ if (IN_PROJECT_ALLOWED_KEYS.has(key)) continue;
5865
+ const denied = KNOWN_DENIED_IN_PROJECT.find((d) => d.key === key);
5866
+ if (!denied) missingFromBoth.push(key);
5867
+ }
5868
+ const staleDenials = KNOWN_DENIED_IN_PROJECT.filter((d) => !KNOWN_CONFIG_TOP_LEVEL_KEYS.has(d.key)).map((d) => d.key);
5869
+ const duplicate = KNOWN_DENIED_IN_PROJECT.filter((d) => IN_PROJECT_ALLOWED_KEYS.has(d.key)).map((d) => d.key);
5870
+ const problems = [];
5871
+ if (missingFromBoth.length > 0) {
5872
+ problems.push(
5873
+ `new Config field(s) not classified as allowed or denied for in-project config: ` + missingFromBoth.join(", ") + ". Add each to IN_PROJECT_ALLOWED_KEYS (if safe) or KNOWN_DENIED_IN_PROJECT (with a reason)."
5874
+ );
5875
+ }
5876
+ if (staleDenials.length > 0) {
5877
+ problems.push(
5878
+ `KNOWN_DENIED_IN_PROJECT references keys that no longer exist on Config: ` + staleDenials.join(", ") + ". Remove them or restore the field on Config."
5879
+ );
5880
+ }
5881
+ if (duplicate.length > 0) {
5882
+ problems.push(
5883
+ `field(s) appear in BOTH IN_PROJECT_ALLOWED_KEYS and KNOWN_DENIED_IN_PROJECT: ` + duplicate.join(", ") + ". The allow-list wins at runtime; remove from one of the two."
5884
+ );
5885
+ }
5886
+ if (problems.length > 0) {
5887
+ throw new Error(
5888
+ `stripUnsafeInProjectFields drift check failed:
5889
+ - ${problems.join("\n - ")}`
5890
+ );
5891
+ }
5892
+ }
5893
+ var driftChecked = false;
5530
5894
  function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
5895
+ if (!driftChecked) {
5896
+ assertInProjectAllowListComplete();
5897
+ driftChecked = true;
5898
+ }
5531
5899
  const stripped = [];
5532
5900
  const out = {};
5533
5901
  for (const [k, v] of Object.entries(inProject)) {
5534
- if (IN_PROJECT_FORBIDDEN_KEYS.has(k)) {
5535
- stripped.push(k);
5902
+ if (IN_PROJECT_ALLOWED_KEYS.has(k)) {
5903
+ out[k] = v;
5536
5904
  continue;
5537
5905
  }
5538
- out[k] = v;
5906
+ stripped.push(k);
5907
+ }
5908
+ const outTools = out["tools"];
5909
+ if (outTools && typeof outTools === "object") {
5910
+ const execCfg = outTools["exec"];
5911
+ if (execCfg && typeof execCfg === "object" && "allow" in execCfg) {
5912
+ const clonedExec = { ...execCfg };
5913
+ delete clonedExec["allow"];
5914
+ out["tools"] = {
5915
+ ...outTools,
5916
+ exec: clonedExec
5917
+ };
5918
+ stripped.push("tools.exec.allow");
5919
+ }
5539
5920
  }
5540
5921
  if (stripped.length > 0) {
5541
5922
  warn(
@@ -5544,7 +5925,7 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
5544
5925
  event: "config.in_project_unsafe_fields_ignored",
5545
5926
  path: sourcePath,
5546
5927
  ignoredKeys: stripped,
5547
- message: `Ignored ${stripped.length} unsafe field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. These can only be set in your personal ~/.wrongstack/config.json, not in a project-committed file.`,
5928
+ message: `Ignored ${stripped.length} field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. Only a small allow-list of benign preferences (model, context, tools limits, features, \u2026) may be set by <project>/.wrongstack/config.json. Everything else must live in your personal ~/.wrongstack/config.json.`,
5548
5929
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5549
5930
  })
5550
5931
  );
@@ -5588,6 +5969,7 @@ var DefaultConfigLoader = class {
5588
5969
  }
5589
5970
  async load(opts = {}) {
5590
5971
  let cfg = { ...BEHAVIOR_DEFAULTS };
5972
+ await this.ensureGlobalDefaults();
5591
5973
  const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
5592
5974
  const [global, local, inProject] = await Promise.all([
5593
5975
  this.readJson(this.paths.globalConfig),
@@ -5652,6 +6034,80 @@ var DefaultConfigLoader = class {
5652
6034
  }
5653
6035
  return Object.freeze(cfg);
5654
6036
  }
6037
+ async ensureGlobalDefaults() {
6038
+ const fp = this.paths.globalConfig;
6039
+ const t0 = Date.now();
6040
+ try {
6041
+ await withFileLock(fp, async () => {
6042
+ let parsed;
6043
+ try {
6044
+ const raw = await fsp2.readFile(fp, "utf8");
6045
+ const result = safeParse(raw);
6046
+ if (!result.ok || !isPlainRecord(result.value)) {
6047
+ return;
6048
+ }
6049
+ parsed = result.value;
6050
+ } catch (err) {
6051
+ if (err.code !== "ENOENT") {
6052
+ this.events?.emit("storage.error", {
6053
+ sessionId: "~config~",
6054
+ store: "config",
6055
+ filePath: fp,
6056
+ operation: "ensure_defaults",
6057
+ outcome: "failure",
6058
+ error: storageErrorString(err),
6059
+ recoverable: false,
6060
+ durationMs: Date.now() - t0,
6061
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
6062
+ });
6063
+ console.warn(JSON.stringify({
6064
+ level: "warn",
6065
+ event: "config.defaults_read_failed",
6066
+ path: fp,
6067
+ message: toErrorMessage(err),
6068
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6069
+ }));
6070
+ return;
6071
+ }
6072
+ parsed = {};
6073
+ }
6074
+ const { value, changed } = fillMissingDefaults(
6075
+ parsed,
6076
+ BEHAVIOR_DEFAULTS
6077
+ );
6078
+ if (!changed) return;
6079
+ await atomicWrite(fp, JSON.stringify(value, null, 2), { mode: 384 });
6080
+ this.events?.emit("storage.write", {
6081
+ sessionId: "~config~",
6082
+ store: "config",
6083
+ filePath: fp,
6084
+ operation: "ensure_defaults",
6085
+ outcome: "success",
6086
+ durationMs: Date.now() - t0,
6087
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
6088
+ });
6089
+ });
6090
+ } catch (err) {
6091
+ this.events?.emit("storage.error", {
6092
+ sessionId: "~config~",
6093
+ store: "config",
6094
+ filePath: fp,
6095
+ operation: "ensure_defaults",
6096
+ outcome: "failure",
6097
+ error: storageErrorString(err),
6098
+ recoverable: false,
6099
+ durationMs: Date.now() - t0,
6100
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
6101
+ });
6102
+ console.warn(JSON.stringify({
6103
+ level: "warn",
6104
+ event: "config.defaults_write_failed",
6105
+ path: fp,
6106
+ message: toErrorMessage(err),
6107
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6108
+ }));
6109
+ }
6110
+ }
5655
6111
  /**
5656
6112
  * Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
5657
6113
  * by the vault (if provided). The file is isolated from the main config
@@ -6117,6 +6573,34 @@ var DefaultSessionReader = class {
6117
6573
  ids = filtered.map((s) => s.id);
6118
6574
  }
6119
6575
  const hits = [];
6576
+ const streaming = this.store.searchEvents?.bind(this.store);
6577
+ if (streaming) {
6578
+ for (const id of ids) {
6579
+ const matched = await streaming(
6580
+ id,
6581
+ (ev) => {
6582
+ if (allowedTypes && !allowedTypes.has(ev.type)) return false;
6583
+ const text = eventText(ev);
6584
+ if (text === null) return false;
6585
+ return matcher(text) !== null;
6586
+ },
6587
+ { limit: limit - hits.length }
6588
+ );
6589
+ for (const m of matched) {
6590
+ const text = expectDefined(eventText(m.event));
6591
+ const hit = expectDefined(matcher(text));
6592
+ hits.push({
6593
+ sessionId: id,
6594
+ eventIndex: m.eventIndex,
6595
+ ts: m.ts,
6596
+ type: m.event.type,
6597
+ snippet: snippetOf(text, hit.start, hit.end)
6598
+ });
6599
+ if (hits.length >= limit) return hits;
6600
+ }
6601
+ }
6602
+ return hits;
6603
+ }
6120
6604
  for (const id of ids) {
6121
6605
  let data;
6122
6606
  try {
@@ -7355,6 +7839,8 @@ var ToolCapabilities = {
7355
7839
  MCP_PROXY: "mcp.proxy",
7356
7840
  /** Can spawn or manage subagents / multi-agent tasks. */
7357
7841
  SUBAGENT_SPAWN: "subagent.spawn",
7842
+ /** Can inspect fleet/subagent coordination state without mutating it. */
7843
+ COORDINATION_FLEET_READ: "coordination.fleet.read",
7358
7844
  /** Can mutate global or session configuration / trust state. */
7359
7845
  CONFIG_MUTATE: "config.mutate",
7360
7846
  /** Can install packages or run package managers with side effects. */
@@ -8468,8 +8954,8 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
8468
8954
  });
8469
8955
  await Promise.race([
8470
8956
  drainPromise,
8471
- new Promise((resolve7) => {
8472
- drainTimer = setTimeout(resolve7, STREAM_DRAIN_TIMEOUT_MS);
8957
+ new Promise((resolve8) => {
8958
+ drainTimer = setTimeout(resolve8, STREAM_DRAIN_TIMEOUT_MS);
8473
8959
  })
8474
8960
  ]);
8475
8961
  } finally {
@@ -8576,7 +9062,7 @@ async function runProviderWithRetry(opts) {
8576
9062
  description
8577
9063
  });
8578
9064
  }
8579
- await new Promise((resolve7, reject) => {
9065
+ await new Promise((resolve8, reject) => {
8580
9066
  let settled = false;
8581
9067
  const cleanup = () => {
8582
9068
  clearTimeout(t);
@@ -8592,7 +9078,7 @@ async function runProviderWithRetry(opts) {
8592
9078
  if (settled) return;
8593
9079
  settled = true;
8594
9080
  cleanup();
8595
- resolve7();
9081
+ resolve8();
8596
9082
  }, delay);
8597
9083
  if (signal.aborted) {
8598
9084
  onAbort();
@@ -9928,6 +10414,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
9928
10414
  level,
9929
10415
  tokens,
9930
10416
  load,
10417
+ hardThreshold: adaptiveThresholds.hard,
9931
10418
  budget,
9932
10419
  signals: { repeatedReadCount: repetition }
9933
10420
  });
@@ -9975,6 +10462,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
9975
10462
  }
9976
10463
  }
9977
10464
  async compact(ctx, aggressive, pressure) {
10465
+ let postCompactionOverflow = null;
9978
10466
  try {
9979
10467
  const report = await this.compactor.compact(ctx, { aggressive });
9980
10468
  this.recordAttempt(pressure.level, pressure.tokens, report);
@@ -10004,6 +10492,38 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
10004
10492
  ...report.collapsedDigest ? { digest: truncateDigest(report.collapsedDigest) } : {}
10005
10493
  });
10006
10494
  ctx.clearFileTracking();
10495
+ const afterTokens = report.fullRequestTokensAfter ?? report.after;
10496
+ const afterLoad = this._maxContext > 0 ? afterTokens / this._maxContext : 0;
10497
+ const stillHard = afterLoad >= pressure.hardThreshold;
10498
+ const fatal = stillHard && (this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard");
10499
+ if (stillHard) {
10500
+ const error = new Error(
10501
+ `Auto-compaction left context above the hard threshold after ${pressure.level} compaction`
10502
+ );
10503
+ this.events?.emit("compaction.failed", {
10504
+ err: error,
10505
+ aggressive,
10506
+ level: pressure.level,
10507
+ tokens: afterTokens,
10508
+ maxContext: this._maxContext,
10509
+ budget: computeContextWindowBudget(ctx, afterTokens, this._maxContext),
10510
+ signals: pressure.signals,
10511
+ load: afterLoad,
10512
+ fatal
10513
+ });
10514
+ if (fatal) {
10515
+ postCompactionOverflow = new AgentError({
10516
+ message: `Auto-compaction did not reduce context below hard threshold`,
10517
+ code: ERROR_CODES.AGENT_CONTEXT_OVERFLOW,
10518
+ recoverable: true,
10519
+ context: {
10520
+ level: pressure.level,
10521
+ tokens: afterTokens,
10522
+ maxContext: this._maxContext
10523
+ }
10524
+ });
10525
+ }
10526
+ }
10007
10527
  } catch (err) {
10008
10528
  const error = err instanceof Error ? err : new Error(String(err));
10009
10529
  const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
@@ -10032,6 +10552,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
10032
10552
  });
10033
10553
  }
10034
10554
  }
10555
+ if (postCompactionOverflow) throw postCompactionOverflow;
10035
10556
  }
10036
10557
  };
10037
10558
  function computeContextWindowBudget(ctx, inputTokens, maxContext) {
@@ -11505,8 +12026,8 @@ ${recentJournal}` : "No prior iterations.",
11505
12026
  await saveGoal(this.goalPath, abandoned, this.opts.events);
11506
12027
  }
11507
12028
  try {
11508
- const { unlink: unlink11 } = await import('fs/promises');
11509
- await unlink11(this.goalPath);
12029
+ const { unlink: unlink13 } = await import('fs/promises');
12030
+ await unlink13(this.goalPath);
11510
12031
  } catch {
11511
12032
  }
11512
12033
  this.opts.onEternalStop?.();
@@ -11892,13 +12413,13 @@ var SubagentBudget = class _SubagentBudget {
11892
12413
  if (!bus?.hasListenerFor("budget.threshold_reached")) {
11893
12414
  return Promise.resolve("stop");
11894
12415
  }
11895
- return new Promise((resolve7) => {
12416
+ return new Promise((resolve8) => {
11896
12417
  let resolved = false;
11897
12418
  const respond = (d) => {
11898
12419
  if (resolved) return;
11899
12420
  resolved = true;
11900
12421
  clearTimeout(fallback);
11901
- resolve7(d);
12422
+ resolve8(d);
11902
12423
  };
11903
12424
  const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
11904
12425
  bus.emit("budget.threshold_reached", {
@@ -15253,14 +15774,15 @@ var SHADOW_AGENT = {
15253
15774
  id: "shadow-agent",
15254
15775
  name: "Shadow",
15255
15776
  role: "shadow-agent",
15256
- prompt: `You are the Shadow Agent \u2014 a silent background monitor for the WrongStack fleet.
15777
+ prompt: `You are the Shadow Agent \u2014 a quiet, one-shot monitor for the WrongStack fleet.
15257
15778
 
15258
- Your job is to observe, detect anomalies, and be ready to intervene \u2014 but only when commanded.
15779
+ 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
15780
 
15260
15781
  ## Core Responsibilities
15261
15782
 
15262
- 1. **Fleet Monitoring** (every 30s)
15263
- - Call \`fleet_status\` + \`fleet_health\` on each heartbeat
15783
+ 1. **Fleet Monitoring** (host-assigned one-shot checks)
15784
+ - The host assigns one-shot check tasks; it does not expect routine heartbeats
15785
+ - On each assigned check, call \`fleet_status\` + \`fleet_health\`
15264
15786
  - Track what each agent is doing (task descriptions)
15265
15787
  - Detect stuck agents (>5min no events), idle agents, crashed agents
15266
15788
 
@@ -15284,31 +15806,30 @@ Your job is to observe, detect anomalies, and be ready to intervene \u2014 but o
15284
15806
  - \`hoop <agentId>\` \u2014 terminate specific agent
15285
15807
  - \`hoop all\` \u2014 terminate all running agents
15286
15808
  - \`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
15809
+ - \`shadow mute\` \u2014 pause anomaly reporting
15810
+ - \`shadow resume\` \u2014 resume anomaly reporting
15811
+ - \`shadow interval <ms>\` \u2014 update the legacy interval setting
15290
15812
  - \`shadow model <model-id>\` \u2014 change analysis model
15291
15813
 
15292
15814
  ## Operating Rules
15293
15815
 
15294
- - **Silent by default**: Use DEBUG level logging unless anomaly detected
15816
+ - **Silent by default**: Do not send mail or status reports for healthy checks
15295
15817
  - **Deterministic**: Same state always produces same actions \u2014 no randomness
15296
- - **Report on anomaly**: When anomaly detected, use \`mail_send\` to broadcast warning
15818
+ - **Report only when needed**: Use \`mail_send\` only for high/critical anomalies or explicit control replies
15297
15819
  - **Never auto-intervene**: Always report unless explicitly commanded
15298
15820
  - **Minimal footprint**: Small state, efficient snapshots
15821
+ - **One-shot lifecycle**: Finish the assigned check and stop; do not schedule follow-up work
15299
15822
 
15300
15823
  ## Startup Sequence
15301
15824
 
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
15825
+ 1. Run one fleet snapshot with \`fleet_status\` + \`fleet_health\`
15826
+ 2. Check \`mail_inbox\` for explicit control messages
15827
+ 3. If healthy, do not send mail; final answer may be exactly \`shadow: quiet\`
15306
15828
 
15307
15829
  ## Shutdown Sequence
15308
15830
 
15309
- 1. Cancel all cron jobs (\`cron_cancel\`)
15310
- 2. Send broadcast: \`shadow:stopped { reason, finalState }\`
15311
- 3. Clean up FleetBus subscriptions`
15831
+ 1. Return only anomalies, command results, or \`shadow: quiet\`
15832
+ 2. The host stops this Shadow Agent after the assigned pass`
15312
15833
  // Budgets are set by the orchestrator per task — see fleet.ts header.
15313
15834
  };
15314
15835
  var CRITIC_AGENT = {
@@ -15363,8 +15884,13 @@ var FLEET_ROSTER_BUDGETS = {
15363
15884
  "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
15364
15885
  "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
15365
15886
  "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
15887
+ "shadow-agent": {
15888
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
15889
+ maxIterations: 2e3,
15890
+ maxToolCalls: 5e3,
15891
+ maxTokens: 6e4,
15892
+ maxCostUsd: 1
15893
+ },
15368
15894
  ...Object.fromEntries(
15369
15895
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
15370
15896
  )
@@ -15803,7 +16329,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
15803
16329
  taskIds.map((id) => {
15804
16330
  const cached = this.completedResults.find((r) => r.taskId === id);
15805
16331
  if (cached) return cached;
15806
- return new Promise((resolve7, reject) => {
16332
+ return new Promise((resolve8, reject) => {
15807
16333
  const timeout = setTimeout(() => {
15808
16334
  this.off("task.completed", handler);
15809
16335
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -15812,7 +16338,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
15812
16338
  if (result.taskId === id) {
15813
16339
  clearTimeout(timeout);
15814
16340
  this.off("task.completed", handler);
15815
- resolve7(result);
16341
+ resolve8(result);
15816
16342
  }
15817
16343
  };
15818
16344
  this.on("task.completed", handler);
@@ -16080,12 +16606,12 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
16080
16606
  }
16081
16607
  return new Promise((resolveDecision) => {
16082
16608
  let settled = false;
16083
- const resolve7 = (d) => {
16609
+ const resolve8 = (d) => {
16084
16610
  if (settled) return;
16085
16611
  settled = true;
16086
16612
  resolveDecision(d);
16087
16613
  };
16088
- const fallback = setTimeout(() => resolve7("stop"), DECISION_TIMEOUT_MS);
16614
+ const fallback = setTimeout(() => resolve8("stop"), DECISION_TIMEOUT_MS);
16089
16615
  budget._events?.emit("budget.threshold_reached", {
16090
16616
  kind: "timeout",
16091
16617
  used,
@@ -16101,11 +16627,11 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
16101
16627
  // disagreeing, resolves as a stop). Async grants still resolve.
16102
16628
  extend: (extra) => {
16103
16629
  clearTimeout(fallback);
16104
- queueMicrotask(() => resolve7({ extend: extra }));
16630
+ queueMicrotask(() => resolve8({ extend: extra }));
16105
16631
  },
16106
16632
  deny: () => {
16107
16633
  clearTimeout(fallback);
16108
- resolve7("stop");
16634
+ resolve8("stop");
16109
16635
  }
16110
16636
  });
16111
16637
  });
@@ -17015,7 +17541,7 @@ var InMemoryAgentBridge = class {
17015
17541
  });
17016
17542
  }
17017
17543
  this.inflightGuards.add(correlationId);
17018
- return new Promise((resolve7, reject) => {
17544
+ return new Promise((resolve8, reject) => {
17019
17545
  const timer = setTimeout(() => {
17020
17546
  this.inflightGuards.delete(correlationId);
17021
17547
  this.pendingRequests.delete(correlationId);
@@ -17034,7 +17560,7 @@ var InMemoryAgentBridge = class {
17034
17560
  return;
17035
17561
  }
17036
17562
  this.pendingRequests.set(correlationId, {
17037
- resolve: resolve7,
17563
+ resolve: resolve8,
17038
17564
  reject,
17039
17565
  timer
17040
17566
  });
@@ -17839,6 +18365,7 @@ function makeSpawnTool(director, roster) {
17839
18365
  usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
17840
18366
  permission: "auto",
17841
18367
  mutating: false,
18368
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
17842
18369
  inputSchema,
17843
18370
  async execute(input) {
17844
18371
  const i = input ?? {};
@@ -17917,6 +18444,7 @@ function makeAssignTool(director) {
17917
18444
  description: "Hand a task to a previously spawned subagent. Returns the task id.",
17918
18445
  permission: "auto",
17919
18446
  mutating: false,
18447
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
17920
18448
  inputSchema,
17921
18449
  async execute(input) {
17922
18450
  const i = input;
@@ -17932,6 +18460,7 @@ function makeAwaitTasksTool(director) {
17932
18460
  description: "Block until every named task completes. Returns the array of TaskResult.",
17933
18461
  permission: "auto",
17934
18462
  mutating: false,
18463
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
17935
18464
  inputSchema: { type: "object", properties: { taskIds: { type: "array", items: { type: "string" }, description: "One or more task ids returned by `assign_task`." } }, required: ["taskIds"] },
17936
18465
  async execute(input) {
17937
18466
  const i = input;
@@ -17946,6 +18475,7 @@ function makeAskTool(director) {
17946
18475
  description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
17947
18476
  permission: "auto",
17948
18477
  mutating: false,
18478
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
17949
18479
  inputSchema: {
17950
18480
  type: "object",
17951
18481
  properties: {
@@ -17981,6 +18511,7 @@ function makeAskResultTool(director) {
17981
18511
  description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
17982
18512
  permission: "auto",
17983
18513
  mutating: false,
18514
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
17984
18515
  inputSchema: {
17985
18516
  type: "object",
17986
18517
  properties: {
@@ -18008,6 +18539,7 @@ function makeRollUpTool(director) {
18008
18539
  description: "Aggregate completed task results into a single formatted summary.",
18009
18540
  permission: "auto",
18010
18541
  mutating: false,
18542
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18011
18543
  inputSchema: {
18012
18544
  type: "object",
18013
18545
  properties: {
@@ -18029,6 +18561,7 @@ function makeTerminateTool(director) {
18029
18561
  description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
18030
18562
  permission: "auto",
18031
18563
  mutating: true,
18564
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18032
18565
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
18033
18566
  async execute(input) {
18034
18567
  const i = input;
@@ -18043,6 +18576,7 @@ function makeTerminateAllTool(director) {
18043
18576
  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
18577
  permission: "auto",
18045
18578
  mutating: true,
18579
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18046
18580
  inputSchema: { type: "object", properties: {}, required: [] },
18047
18581
  async execute() {
18048
18582
  await director.terminateAll();
@@ -18056,6 +18590,7 @@ function makeFleetStatusTool(director) {
18056
18590
  description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
18057
18591
  permission: "auto",
18058
18592
  mutating: false,
18593
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18059
18594
  inputSchema: { type: "object", properties: {}, required: [] },
18060
18595
  async execute() {
18061
18596
  const base = director.status();
@@ -18077,6 +18612,7 @@ function makeFleetUsageTool(director) {
18077
18612
  description: "Token + cost breakdown across the fleet, per-subagent and totals.",
18078
18613
  permission: "auto",
18079
18614
  mutating: false,
18615
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18080
18616
  inputSchema: { type: "object", properties: {}, required: [] },
18081
18617
  async execute() {
18082
18618
  return director.snapshot();
@@ -18089,6 +18625,7 @@ function makeFleetSessionTool(director) {
18089
18625
  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
18626
  permission: "auto",
18091
18627
  mutating: false,
18628
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18092
18629
  inputSchema: {
18093
18630
  type: "object",
18094
18631
  properties: {
@@ -18115,6 +18652,7 @@ function makeFleetHealthTool(director) {
18115
18652
  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
18653
  permission: "auto",
18117
18654
  mutating: false,
18655
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18118
18656
  inputSchema: { type: "object", properties: {}, required: [] },
18119
18657
  async execute() {
18120
18658
  const status = director.status();
@@ -18145,6 +18683,7 @@ function makeCollabDebugTool(director) {
18145
18683
  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
18684
  permission: "auto",
18147
18685
  mutating: false,
18686
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18148
18687
  inputSchema: {
18149
18688
  type: "object",
18150
18689
  properties: {
@@ -18208,6 +18747,7 @@ function makeFleetEmitTool(director) {
18208
18747
  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
18748
  permission: "auto",
18210
18749
  mutating: false,
18750
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
18211
18751
  inputSchema: {
18212
18752
  type: "object",
18213
18753
  properties: {
@@ -18240,6 +18780,7 @@ function makeWorkCompleteTool(director) {
18240
18780
  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
18781
  permission: "auto",
18242
18782
  mutating: false,
18783
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
18243
18784
  inputSchema: { type: "object", properties: {}, required: [] },
18244
18785
  async execute() {
18245
18786
  director.workComplete();
@@ -18689,6 +19230,9 @@ var Director = class _Director {
18689
19230
  /** Snapshot of which subagent owns each task — drives state-checkpoint
18690
19231
  * status updates without re-walking the manifest. */
18691
19232
  taskOwners = /* @__PURE__ */ new Map();
19233
+ /** Infrastructure-owned task ids that should not appear in user-visible
19234
+ * manifest/session/checkpoint/rollup state. */
19235
+ internalTaskIds = /* @__PURE__ */ new Set();
18692
19236
  /** Cumulative auto-extension grants per subagent (all budget kinds). Lets
18693
19237
  * /fleet render "⚡ extended ×N" without replaying the event stream. */
18694
19238
  extendTotals = /* @__PURE__ */ new Map();
@@ -18802,17 +19346,21 @@ var Director = class _Director {
18802
19346
  this.fleetManager?.setCoordinator(this.coordinator);
18803
19347
  this.taskCompletedListener = (payload) => {
18804
19348
  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);
19349
+ const internalTask = this.internalTaskIds.delete(r.taskId);
19350
+ if (!internalTask) {
19351
+ this.completed.set(r.taskId, r);
19352
+ if (this.completed.size > _Director.MAX_COMPLETED) {
19353
+ const toDelete = this.completed.size - _Director.MAX_COMPLETED;
19354
+ const keys = [...this.completed.keys()].slice(0, toDelete);
19355
+ for (const k of keys) this.completed.delete(k);
19356
+ }
18810
19357
  }
18811
19358
  const waiter = this.taskWaiters.get(r.taskId);
18812
19359
  if (waiter) {
18813
19360
  waiter.resolve(r);
18814
19361
  this.taskWaiters.delete(r.taskId);
18815
19362
  }
19363
+ if (internalTask) return;
18816
19364
  const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
18817
19365
  const failed = r.status !== "success";
18818
19366
  const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
@@ -19482,6 +20030,23 @@ var Director = class _Director {
19482
20030
  this.scheduleManifest();
19483
20031
  return taskWithId.id;
19484
20032
  }
20033
+ /**
20034
+ * Assign infrastructure-owned work directly to the coordinator without
20035
+ * manifest/session/checkpoint bookkeeping. The task still uses the normal
20036
+ * subagent runner, budget, and completion events, but it is excluded from
20037
+ * rollups and persisted fleet task history.
20038
+ */
20039
+ async assignInternal(task) {
20040
+ const taskWithId = task.id ? task : { ...task, id: randomUUID() };
20041
+ this.internalTaskIds.add(taskWithId.id);
20042
+ try {
20043
+ await this.coordinator.assign(taskWithId);
20044
+ } catch (err) {
20045
+ this.internalTaskIds.delete(taskWithId.id);
20046
+ throw err;
20047
+ }
20048
+ return taskWithId.id;
20049
+ }
19485
20050
  /**
19486
20051
  * Block until every task id resolves. Returns results in the same
19487
20052
  * order as the input. If any task hasn't completed by the time this
@@ -19496,11 +20061,11 @@ var Director = class _Director {
19496
20061
  if (cached) return cached;
19497
20062
  const existing = this.taskWaiters.get(id);
19498
20063
  if (existing) return existing.promise;
19499
- let resolve7;
20064
+ let resolve8;
19500
20065
  const promise = new Promise((res) => {
19501
- resolve7 = res;
20066
+ resolve8 = res;
19502
20067
  });
19503
- this.taskWaiters.set(id, { promise, resolve: resolve7 });
20068
+ this.taskWaiters.set(id, { promise, resolve: resolve8 });
19504
20069
  return promise;
19505
20070
  })
19506
20071
  );
@@ -19896,7 +20461,7 @@ function createDelegateTool(opts) {
19896
20461
  subagentId
19897
20462
  });
19898
20463
  const dir = director;
19899
- const result = await new Promise((resolve7) => {
20464
+ const result = await new Promise((resolve8) => {
19900
20465
  let settled = false;
19901
20466
  let timer;
19902
20467
  const finish = (value) => {
@@ -19906,7 +20471,7 @@ function createDelegateTool(opts) {
19906
20471
  offTool();
19907
20472
  offIter();
19908
20473
  offProgress();
19909
- resolve7(value);
20474
+ resolve8(value);
19910
20475
  };
19911
20476
  const arm = () => {
19912
20477
  if (timer) clearTimeout(timer);
@@ -21238,6 +21803,14 @@ var SpecParser = class {
21238
21803
  };
21239
21804
 
21240
21805
  // src/sdd/task-generator.ts
21806
+ function extractVerificationCommand(criteria) {
21807
+ const marker = /^\s*(?:\$\s+|(?:run|verify|cmd)\s*:\s*)(.+\S)\s*$/i;
21808
+ for (const c of criteria) {
21809
+ const m = marker.exec(c);
21810
+ if (m?.[1]) return m[1].trim();
21811
+ }
21812
+ return void 0;
21813
+ }
21241
21814
  var TaskGenerator = class {
21242
21815
  constructor(opts) {
21243
21816
  this.opts = opts;
@@ -21245,15 +21818,18 @@ var TaskGenerator = class {
21245
21818
  opts;
21246
21819
  async generateFromSpec(spec) {
21247
21820
  const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
21821
+ const featureIds = [];
21248
21822
  const overview = spec.sections.find((s) => s.type === "overview");
21249
21823
  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
- });
21824
+ featureIds.push(
21825
+ this.opts.taskTracker.addNode({
21826
+ title: `Implement ${spec.title}`,
21827
+ description: overview.content,
21828
+ type: "feature",
21829
+ priority: "high",
21830
+ status: "pending"
21831
+ }).id
21832
+ );
21257
21833
  }
21258
21834
  const byPriority = {
21259
21835
  critical: [],
@@ -21268,7 +21844,7 @@ var TaskGenerator = class {
21268
21844
  const order = ["critical", "high", "medium", "low"];
21269
21845
  for (const p of order) {
21270
21846
  for (const req of byPriority[p]) {
21271
- this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
21847
+ featureIds.push(this.opts.taskTracker.addNode(this.createTaskFromRequirement(req)).id);
21272
21848
  }
21273
21849
  }
21274
21850
  if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
@@ -21279,31 +21855,37 @@ var TaskGenerator = class {
21279
21855
  priority: "high",
21280
21856
  status: "pending"
21281
21857
  });
21858
+ featureIds.push(apiParent.id);
21282
21859
  for (const endpoint of spec.apiEndpoints) {
21283
21860
  const task = this.createTaskFromEndpoint(endpoint);
21284
- this.opts.taskTracker.addNode({
21285
- ...task,
21286
- parentId: apiParent.id
21287
- });
21861
+ featureIds.push(
21862
+ this.opts.taskTracker.addNode({
21863
+ ...task,
21864
+ parentId: apiParent.id
21865
+ }).id
21866
+ );
21288
21867
  }
21289
21868
  }
21290
- this.opts.taskTracker.addNode({
21869
+ const testId = this.opts.taskTracker.addNode({
21291
21870
  title: "Write Tests",
21292
21871
  description: "Comprehensive test coverage for all features",
21293
21872
  type: "test",
21294
21873
  priority: "high",
21295
21874
  status: "pending"
21296
- });
21297
- this.opts.taskTracker.addNode({
21875
+ }).id;
21876
+ for (const f of featureIds) this.opts.taskTracker.addDependency(f, testId);
21877
+ const docsId = this.opts.taskTracker.addNode({
21298
21878
  title: "Update Documentation",
21299
21879
  description: "Update docs for new features",
21300
21880
  type: "docs",
21301
21881
  priority: "medium",
21302
21882
  status: "pending"
21303
- });
21883
+ }).id;
21884
+ for (const f of [...featureIds, testId]) this.opts.taskTracker.addDependency(f, docsId);
21304
21885
  return graph;
21305
21886
  }
21306
21887
  createTaskFromRequirement(req) {
21888
+ const verificationCommand = this.opts.verificationFromAcceptance ? extractVerificationCommand(req.acceptanceCriteria) : void 0;
21307
21889
  return {
21308
21890
  title: req.description,
21309
21891
  description: this.buildDescription(req),
@@ -21312,7 +21894,8 @@ var TaskGenerator = class {
21312
21894
  status: "pending",
21313
21895
  specRequirementId: req.id,
21314
21896
  tags: [req.type, req.priority],
21315
- estimateHours: this.estimateHours(req)
21897
+ estimateHours: this.estimateHours(req),
21898
+ ...verificationCommand ? { metadata: { verificationCommand } } : {}
21316
21899
  };
21317
21900
  }
21318
21901
  createTaskFromEndpoint(endpoint) {
@@ -21501,6 +22084,27 @@ var TaskTracker = class {
21501
22084
  opts;
21502
22085
  graph = null;
21503
22086
  transitions = [];
22087
+ listeners = [];
22088
+ /**
22089
+ * Subscribe to live task mutations (add / update / status change). Returns an
22090
+ * unsubscribe fn. This is the hook the board projector uses to stream a live
22091
+ * snapshot — the tracker was previously fire-and-forget with no observability.
22092
+ */
22093
+ subscribe(listener) {
22094
+ this.listeners.push(listener);
22095
+ return () => {
22096
+ const i = this.listeners.indexOf(listener);
22097
+ if (i >= 0) this.listeners.splice(i, 1);
22098
+ };
22099
+ }
22100
+ notifyChange(change) {
22101
+ for (const l of this.listeners) {
22102
+ try {
22103
+ l(change);
22104
+ } catch {
22105
+ }
22106
+ }
22107
+ }
21504
22108
  /**
21505
22109
  * Attach an existing graph (used by PhaseOrchestrator to associate a tracker
21506
22110
  * with a phase's pre-built task graph without re-creating it).
@@ -21545,6 +22149,7 @@ var TaskTracker = class {
21545
22149
  }
21546
22150
  this.graph.updatedAt = now;
21547
22151
  this.persist();
22152
+ this.notifyChange({ type: "node_added", nodeId: newNode.id, node: newNode });
21548
22153
  return newNode;
21549
22154
  }
21550
22155
  addEdge(from, to, type = "depends_on") {
@@ -21561,6 +22166,68 @@ var TaskTracker = class {
21561
22166
  this.graph.updatedAt = Date.now();
21562
22167
  this.persist();
21563
22168
  }
22169
+ /**
22170
+ * Declare that `taskId` depends on `depId` (a `depends_on` edge `depId → taskId`),
22171
+ * guarding against self-loops, duplicates, missing nodes, and cycles. Returns
22172
+ * true if the dependency now holds (added or already present), false if it was
22173
+ * rejected (would create a cycle / unknown node). This is the safe entry point
22174
+ * for wiring agent-declared `dependsOn` references into the graph.
22175
+ */
22176
+ addDependency(depId, taskId) {
22177
+ if (!this.graph) return false;
22178
+ if (depId === taskId) return false;
22179
+ if (!this.graph.nodes.has(depId) || !this.graph.nodes.has(taskId)) return false;
22180
+ if (this.getBlockers(taskId).includes(depId)) return true;
22181
+ if (this.dependsOnTransitively(depId, taskId, /* @__PURE__ */ new Set())) return false;
22182
+ this.addEdge(depId, taskId, "depends_on");
22183
+ return true;
22184
+ }
22185
+ /** True when `taskId` transitively depends on `targetId` (follows depends_on blockers). */
22186
+ dependsOnTransitively(taskId, targetId, seen) {
22187
+ if (taskId === targetId) return true;
22188
+ if (seen.has(taskId)) return false;
22189
+ seen.add(taskId);
22190
+ for (const blocker of this.getBlockers(taskId)) {
22191
+ if (this.dependsOnTransitively(blocker, targetId, seen)) return true;
22192
+ }
22193
+ return false;
22194
+ }
22195
+ /**
22196
+ * Merge `patch` into a node's `metadata` (used for per-task model/provider/
22197
+ * fallback assignment and the cancel marker). Persists + notifies as a node
22198
+ * update. No-op if the node is missing.
22199
+ */
22200
+ patchMetadata(id, patch) {
22201
+ if (!this.graph) return;
22202
+ const node = this.graph.nodes.get(id);
22203
+ if (!node) return;
22204
+ node.metadata = { ...node.metadata, ...patch };
22205
+ node.updatedAt = Date.now();
22206
+ this.graph.updatedAt = node.updatedAt;
22207
+ this.persist();
22208
+ this.notifyChange({ type: "node_updated", nodeId: id, node });
22209
+ }
22210
+ /**
22211
+ * Remove a node and every edge touching it. Intended for deleting a task that
22212
+ * has not started yet — callers must gate on status (do not remove a running
22213
+ * task). Dependents simply lose this blocker (re-evaluated by `canStart`).
22214
+ * Returns true if a node was removed.
22215
+ */
22216
+ removeNode(id) {
22217
+ if (!this.graph) return false;
22218
+ const node = this.graph.nodes.get(id);
22219
+ if (!node) return false;
22220
+ this.graph.nodes.delete(id);
22221
+ this.graph.edges = this.graph.edges.filter((e) => e.from !== id && e.to !== id);
22222
+ this.graph.rootNodes = this.graph.rootNodes.filter((r) => r !== id);
22223
+ for (const n of this.graph.nodes.values()) {
22224
+ if (n.children?.includes(id)) n.children = n.children.filter((c) => c !== id);
22225
+ }
22226
+ this.graph.updatedAt = Date.now();
22227
+ this.persist();
22228
+ this.notifyChange({ type: "node_removed", nodeId: id, node });
22229
+ return true;
22230
+ }
21564
22231
  updateNodeStatus(id, status, reason) {
21565
22232
  if (!this.graph) throw new SddError({
21566
22233
  message: "No graph loaded",
@@ -21592,6 +22259,12 @@ var TaskTracker = class {
21592
22259
  }
21593
22260
  this.graph.updatedAt = now;
21594
22261
  this.persist();
22262
+ this.notifyChange({
22263
+ type: "status_changed",
22264
+ nodeId: id,
22265
+ node,
22266
+ transition: { from, to: status, timestamp: now, reason }
22267
+ });
21595
22268
  }
21596
22269
  updateNode(id, patch) {
21597
22270
  if (!this.graph) throw new SddError({
@@ -21609,9 +22282,11 @@ var TaskTracker = class {
21609
22282
  if (patch.priority !== void 0) node.priority = patch.priority;
21610
22283
  if (patch.estimateHours !== void 0) node.estimateHours = patch.estimateHours;
21611
22284
  if (patch.tags !== void 0) node.tags = patch.tags;
22285
+ if (patch.assignee !== void 0) node.assignee = patch.assignee;
21612
22286
  node.updatedAt = Date.now();
21613
22287
  this.graph.updatedAt = node.updatedAt;
21614
22288
  this.persist();
22289
+ this.notifyChange({ type: "node_updated", nodeId: id, node });
21615
22290
  }
21616
22291
  getNode(id) {
21617
22292
  return this.graph?.nodes.get(id);
@@ -21790,7 +22465,10 @@ var TaskFlow = class {
21790
22465
  throw err;
21791
22466
  }
21792
22467
  this.setPhase("generating");
21793
- const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
22468
+ const generator = new TaskGenerator({
22469
+ taskTracker: this.opts.tracker,
22470
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
22471
+ });
21794
22472
  this.graph = await generator.generateFromSpec(this.spec);
21795
22473
  return this.graph;
21796
22474
  }
@@ -22269,27 +22947,37 @@ function buildImplementationPrompt(session) {
22269
22947
  "```json",
22270
22948
  "[",
22271
22949
  " {",
22950
+ ' "id": "t1",',
22272
22951
  ' "title": "Create auth middleware",',
22273
22952
  ' "description": "Implement JWT verification middleware for protected routes",',
22274
22953
  ' "type": "feature",',
22275
22954
  ' "priority": "critical",',
22276
22955
  ' "estimateHours": 3,',
22956
+ ' "dependsOn": [],',
22277
22957
  ' "tags": ["auth", "middleware"]',
22278
22958
  " },",
22279
22959
  " {",
22960
+ ' "id": "t2",',
22280
22961
  ' "title": "Write auth tests",',
22281
22962
  ' "description": "Unit and integration tests for authentication flow",',
22282
22963
  ' "type": "test",',
22283
22964
  ' "priority": "high",',
22284
22965
  ' "estimateHours": 2,',
22966
+ ' "dependsOn": ["t1"],',
22285
22967
  ' "tags": ["test", "auth"]',
22286
22968
  " }",
22287
22969
  "]",
22288
22970
  "```",
22289
22971
  "",
22290
22972
  "Rules:",
22291
- "- Each task must be independently executable",
22292
- "- Order tasks by dependency (things that block others come first)",
22973
+ '- Give every task a short stable "id" (t1, t2, \u2026). Reference prerequisites in "dependsOn"',
22974
+ " as a list of those ids \u2014 this builds the real dependency graph that drives parallel vs",
22975
+ " sequential execution.",
22976
+ '- "dependsOn": [] means the task is independent and may run in parallel with other roots.',
22977
+ "- A task with dependsOn runs ONLY after every listed task completes. Model true ordering:",
22978
+ " tests depend on the feature they test, docs/integration depend on the parts they cover.",
22979
+ "- Do NOT create cycles (t1\u2192t2\u2192t1). Keep chains as shallow as correctness allows so",
22980
+ " independent work runs concurrently.",
22293
22981
  '- Use type: "feature" for code, "test" for tests, "docs" for documentation, "chore" for config',
22294
22982
  '- Use priority: "critical" for blockers, "high" for core features, "medium" for nice-to-haves, "low" for polish'
22295
22983
  ].join("\n");
@@ -22358,10 +23046,10 @@ var AISpecBuilder = class {
22358
23046
  async saveSession() {
22359
23047
  if (!this.sessionPath) return;
22360
23048
  try {
22361
- const fsp16 = await import('fs/promises');
22362
- const path22 = await import('path');
23049
+ const fsp18 = await import('fs/promises');
23050
+ const path23 = await import('path');
22363
23051
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
22364
- await fsp16.mkdir(path22.dirname(this.sessionPath), { recursive: true });
23052
+ await fsp18.mkdir(path23.dirname(this.sessionPath), { recursive: true });
22365
23053
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
22366
23054
  } catch {
22367
23055
  }
@@ -22370,8 +23058,8 @@ var AISpecBuilder = class {
22370
23058
  async loadSession() {
22371
23059
  if (!this.sessionPath) return false;
22372
23060
  try {
22373
- const fsp16 = await import('fs/promises');
22374
- const raw = await fsp16.readFile(this.sessionPath, "utf8");
23061
+ const fsp18 = await import('fs/promises');
23062
+ const raw = await fsp18.readFile(this.sessionPath, "utf8");
22375
23063
  const loaded = JSON.parse(raw);
22376
23064
  if (loaded?.id && loaded?.phase && loaded?.title) {
22377
23065
  this.session = loaded;
@@ -22385,8 +23073,8 @@ var AISpecBuilder = class {
22385
23073
  async deleteSession() {
22386
23074
  if (!this.sessionPath) return;
22387
23075
  try {
22388
- const fsp16 = await import('fs/promises');
22389
- await fsp16.unlink(this.sessionPath);
23076
+ const fsp18 = await import('fs/promises');
23077
+ await fsp18.unlink(this.sessionPath);
22390
23078
  } catch {
22391
23079
  }
22392
23080
  }
@@ -23088,15 +23776,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
23088
23776
  maxId = id;
23089
23777
  }
23090
23778
  }
23091
- const path22 = [];
23779
+ const path23 = [];
23092
23780
  let current = maxId;
23093
23781
  const visited = /* @__PURE__ */ new Set();
23094
23782
  while (current && !visited.has(current)) {
23095
23783
  visited.add(current);
23096
- path22.unshift(current);
23784
+ path23.unshift(current);
23097
23785
  current = prev.get(current) ?? null;
23098
23786
  }
23099
- return path22;
23787
+ return path23;
23100
23788
  }
23101
23789
  function computeParallelGroups(graph, blockedByMap) {
23102
23790
  const groups = [];
@@ -23515,6 +24203,24 @@ var SddTaskDecomposer = class {
23515
24203
  getWaveCount() {
23516
24204
  return this.wave;
23517
24205
  }
24206
+ /**
24207
+ * All ready (dependency-satisfied) pending tasks, priority-sorted — UNSLICED.
24208
+ * The continuous scheduler fills its own free slots from this list, so unlike
24209
+ * `nextBatch()` it does not cap at `slots`.
24210
+ */
24211
+ readyNodes() {
24212
+ return this.pendingReadyNodes();
24213
+ }
24214
+ /**
24215
+ * True when every node has reached a terminal state (completed or failed).
24216
+ * This — not `isDone()` (which requires ALL completed) — is the correct loop
24217
+ * exit for the continuous scheduler: a terminally-failed task must not keep
24218
+ * the run spinning to its backstop.
24219
+ */
24220
+ isSettled() {
24221
+ const nodes = this.tracker.getAllNodes();
24222
+ return nodes.length > 0 && nodes.every((n) => n.status === "completed" || n.status === "failed");
24223
+ }
23518
24224
  // -------------------------------------------------------------------
23519
24225
  // Internal helpers
23520
24226
  // -------------------------------------------------------------------
@@ -23554,73 +24260,493 @@ var SddTaskDecomposer = class {
23554
24260
  var SddParallelRun = class {
23555
24261
  constructor(opts) {
23556
24262
  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);
24263
+ this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 2));
24264
+ this.timeoutMs = opts.taskTimeoutMs;
24265
+ this.idleTimeoutMs = Math.max(1, opts.taskIdleTimeoutMs ?? 6e5);
24266
+ this.maxRetries = Math.max(0, opts.maxRetries ?? 3);
24267
+ this.maxSupervisorEscalations = Math.max(0, opts.maxSupervisorEscalations ?? 2);
24268
+ this.maxFailedSweeps = Math.max(0, opts.maxFailedRetrySweeps ?? 2);
24269
+ this.runId = opts.runId ?? `sdd-${randomUUID().slice(0, 8)}`;
24270
+ this.events = opts.events;
24271
+ this.maxTotalWaves = opts.maxTotalWaves ?? opts.graph.nodes.size * (this.maxRetries + 2) + 10;
24272
+ this.maxWallClockMs = opts.maxWallClockMs;
24273
+ this.maxRecoveryRounds = Math.max(0, opts.maxRecoveryRounds ?? 0);
23560
24274
  this.decomposer = new SddTaskDecomposer(opts.tracker, opts.graph, { parallelSlots: this.slots });
23561
24275
  }
23562
24276
  opts;
23563
24277
  slots;
24278
+ /** Opt-in hard wall-clock cap (undefined → no cap; idle reaper guards instead). */
23564
24279
  timeoutMs;
24280
+ /** Idle reaper window (ms) — resets on activity; reaps only a genuine stall. */
24281
+ idleTimeoutMs;
23565
24282
  maxRetries;
24283
+ /** Max supervisor rescues per task before it must terminal-fail (loop guard). */
24284
+ maxSupervisorEscalations;
24285
+ /** Per-task count of supervisor rescues used (resets nothing — bounds the loop). */
24286
+ supervisorEscalations = /* @__PURE__ */ new Map();
24287
+ /** Max end-of-run failed-task sweeps (see `maxFailedRetrySweeps`). */
24288
+ maxFailedSweeps;
24289
+ /** How many failed-task sweeps have run this `run()` so far. */
24290
+ failedSweeps = 0;
24291
+ /** Completed-count snapshot at the last sweep, to detect a no-progress sweep. */
24292
+ lastSweepCompleted = 0;
23566
24293
  decomposer;
23567
24294
  coordinator = null;
23568
24295
  stopRequested = false;
23569
24296
  retryMap = /* @__PURE__ */ new Map();
24297
+ runId;
24298
+ events;
24299
+ maxTotalWaves;
24300
+ maxWallClockMs;
24301
+ maxRecoveryRounds;
24302
+ recoveryRounds = 0;
24303
+ /** Per-run worker identities, so the board shows "who is on what". */
24304
+ usedNicknames = /* @__PURE__ */ new Set();
24305
+ /** Per-task git worktree cwd (Layer 2 worktree isolation; empty otherwise). */
24306
+ taskCwds = /* @__PURE__ */ new Map();
24307
+ /** Per-task git worktree branch, for board display. */
24308
+ taskBranches = /* @__PURE__ */ new Map();
24309
+ /** Live worktree handles keyed by task id (for commit/merge/release). */
24310
+ taskWorktrees = /* @__PURE__ */ new Map();
24311
+ /** Live subagent id per running task — lets cancelTask() abort exactly one. */
24312
+ taskSubagents = /* @__PURE__ */ new Map();
24313
+ /** Tasks the user cancelled mid-flight — skip retry, mark terminal-cancelled. */
24314
+ cancelledTasks = /* @__PURE__ */ new Set();
24315
+ /**
24316
+ * Base branch the run's squash commits land on (captured once at start when
24317
+ * worktrees are enabled). Anchors a later `rollback()`.
24318
+ */
24319
+ baseBranch;
24320
+ /**
24321
+ * Squash-merge commits this run landed on the base branch, in landing order.
24322
+ * `rollback()` reverts these (newest → oldest). Persisted via the board
24323
+ * snapshot so a post-run rollback can read them off disk.
24324
+ */
24325
+ mergedCommits = [];
24326
+ /** Monotonic dispatch counter (unique subagent ids) + dispatch-round counter. */
24327
+ dispatchSeq = 0;
24328
+ round = 0;
24329
+ /** Type-safe emit on the optional EventBus (no-op when unwired). */
24330
+ emit(event, payload) {
24331
+ this.events?.emit(event, payload);
24332
+ }
23570
24333
  // -------------------------------------------------------------------
23571
24334
  // Public API
23572
24335
  // -------------------------------------------------------------------
24336
+ paused = false;
23573
24337
  /** Trigger stop — causes run() to abort after the current wave. */
23574
24338
  stop() {
23575
24339
  this.stopRequested = true;
24340
+ this.paused = false;
23576
24341
  this.coordinator?.stopAll();
23577
24342
  }
23578
- /** Execute all waves until completion or deadlock. Returns final summary. */
23579
- async run() {
23580
- this.stopRequested = false;
23581
- this.retryMap.clear();
23582
- const startTime = Date.now();
23583
- let totalCompleted = 0;
23584
- let totalFailed = 0;
23585
- let totalWaves = 0;
23586
- this.buildCoordinator();
23587
- while (!this.stopRequested && !this.decomposer.isDone()) {
23588
- const batch = this.decomposer.nextBatch();
23589
- if (batch.deadlocked) {
23590
- break;
23591
- }
23592
- if (batch.tasks.length === 0 && batch.allDone) {
23593
- break;
23594
- }
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;
23604
- }
23605
- const finalProgress = this.opts.tracker.getProgress();
23606
- return {
23607
- totalWaves,
23608
- totalCompleted,
23609
- totalFailed,
23610
- totalDurationMs: Date.now() - startTime,
23611
- deadlocked: !this.decomposer.isDone() && this.stopRequested === false,
23612
- stopRequested: this.stopRequested,
23613
- finalProgress
23614
- };
24343
+ /** Pause: no new wave starts until resume() (the current wave finishes). */
24344
+ pause() {
24345
+ this.paused = true;
23615
24346
  }
23616
- // -------------------------------------------------------------------
23617
- // Internal
23618
- // -------------------------------------------------------------------
23619
- buildCoordinator() {
23620
- const config = {
23621
- coordinatorId: `sdd-parallel-${randomUUID().slice(0, 8)}`,
23622
- maxConcurrent: this.slots,
23623
- doneCondition: { type: "all_tasks_done" }
24347
+ resume() {
24348
+ this.paused = false;
24349
+ }
24350
+ isPaused() {
24351
+ return this.paused;
24352
+ }
24353
+ isRunning() {
24354
+ return !this.stopRequested && !this.decomposer.isSettled();
24355
+ }
24356
+ /** Base branch the run's squash commits land on (undefined when worktrees off). */
24357
+ getBaseBranch() {
24358
+ return this.baseBranch;
24359
+ }
24360
+ /** Squash commits this run landed on the base branch, in landing order. */
24361
+ getMergedCommits() {
24362
+ return this.mergedCommits;
24363
+ }
24364
+ /**
24365
+ * Remove every git worktree + branch this run (and any prior run) created.
24366
+ * Refuses while the run is still live — cleaning a checkout under an active
24367
+ * worker would corrupt it. Stop first. Returns the number of worktrees removed
24368
+ * (0 when worktrees are disabled). Idempotent.
24369
+ */
24370
+ async cleanupWorktrees() {
24371
+ if (this.isRunning()) return 0;
24372
+ const wt = this.opts.worktrees;
24373
+ if (!wt) return 0;
24374
+ for (const [taskId, handle] of [...this.taskWorktrees]) {
24375
+ await wt.release(handle, { keep: false }).catch(() => {
24376
+ });
24377
+ this.forgetWorktree(taskId);
24378
+ }
24379
+ const { removed } = await wt.cleanupAllManaged();
24380
+ return removed;
24381
+ }
24382
+ /**
24383
+ * Undo the run's merged commits by reverting each on the base branch (history
24384
+ * preserving). Refuses while the run is still live (stop first). Returns the
24385
+ * revert outcome; a dirty tree or revert conflict surfaces as `ok:false`.
24386
+ */
24387
+ async rollback() {
24388
+ if (this.isRunning()) return { ok: false, reverted: 0, reason: "run still active \u2014 stop it first" };
24389
+ const wt = this.opts.worktrees;
24390
+ if (!wt || !this.baseBranch) {
24391
+ return { ok: false, reverted: 0, reason: "no worktree run to roll back" };
24392
+ }
24393
+ return wt.revertCommits(
24394
+ this.baseBranch,
24395
+ this.mergedCommits.map((c) => c.sha)
24396
+ );
24397
+ }
24398
+ /** Requeue a task to `pending` so the scheduler re-runs it (clears retries + cancel marker). */
24399
+ retryTask(taskId) {
24400
+ if (!this.opts.tracker.getNode(taskId)) return false;
24401
+ this.retryMap.delete(taskId);
24402
+ this.persistRetries(taskId, 0);
24403
+ this.cancelledTasks.delete(taskId);
24404
+ this.opts.tracker.patchMetadata(taskId, { cancelled: void 0 });
24405
+ this.opts.tracker.updateNodeStatus(taskId, "pending", "manual retry");
24406
+ return true;
24407
+ }
24408
+ /** Reassign a task to a specific agent name (reflected on the board). */
24409
+ reassignTask(taskId, agentName) {
24410
+ if (!this.opts.tracker.getNode(taskId)) return false;
24411
+ this.opts.tracker.updateNode(taskId, { assignee: agentName });
24412
+ return true;
24413
+ }
24414
+ /**
24415
+ * Set/override a task's worker model (and optionally provider) — applied on its
24416
+ * NEXT dispatch (a running task must be cancelled + retried to take effect). The
24417
+ * assignment lives on node metadata so it survives crash → resume.
24418
+ */
24419
+ setTaskModel(taskId, model, provider) {
24420
+ if (!this.opts.tracker.getNode(taskId)) return false;
24421
+ this.opts.tracker.patchMetadata(taskId, { model, ...provider !== void 0 ? { provider } : {} });
24422
+ return true;
24423
+ }
24424
+ /** Set/override a task's fallback model chain (applied on its next dispatch). */
24425
+ setTaskFallbacks(taskId, fallbackModels) {
24426
+ if (!this.opts.tracker.getNode(taskId)) return false;
24427
+ this.opts.tracker.patchMetadata(taskId, { fallbackModels });
24428
+ return true;
24429
+ }
24430
+ /**
24431
+ * Set/override a task's verification command (the completion gate runs it in
24432
+ * the task's cwd and only lets the task complete on exit 0). Empty/undefined
24433
+ * clears it. Applied on the task's next verification — i.e. its next dispatch.
24434
+ */
24435
+ setTaskVerification(taskId, verificationCommand) {
24436
+ if (!this.opts.tracker.getNode(taskId)) return false;
24437
+ const cmd = verificationCommand?.trim();
24438
+ this.opts.tracker.patchMetadata(taskId, { verificationCommand: cmd ? cmd : void 0 });
24439
+ return true;
24440
+ }
24441
+ /**
24442
+ * Cancel a task. If it is currently running, abort its subagent and mark the
24443
+ * node terminally failed+cancelled (so the scheduler frees the slot and does
24444
+ * NOT retry it). If it has not started, it is simply marked cancelled. Use
24445
+ * `retryTask` to bring a cancelled task back. Returns false for an unknown task.
24446
+ */
24447
+ async cancelTask(taskId) {
24448
+ const node = this.opts.tracker.getNode(taskId);
24449
+ if (!node) return false;
24450
+ this.cancelledTasks.add(taskId);
24451
+ this.opts.tracker.patchMetadata(taskId, { cancelled: true });
24452
+ this.opts.tracker.updateNodeStatus(taskId, "failed", "cancelled by user");
24453
+ this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId: "", error: "cancelled by user" });
24454
+ const subagentId = this.taskSubagents.get(taskId);
24455
+ if (subagentId && this.coordinator) {
24456
+ await this.coordinator.stop(subagentId).catch(() => {
24457
+ });
24458
+ }
24459
+ return true;
24460
+ }
24461
+ /**
24462
+ * Delete a not-yet-started task from the graph (pending/blocked/failed only —
24463
+ * never a running task; cancel it first). Removes the node and every edge
24464
+ * touching it; dependents lose this blocker. Returns false if missing or running.
24465
+ */
24466
+ deleteTask(taskId) {
24467
+ const node = this.opts.tracker.getNode(taskId);
24468
+ if (!node) return false;
24469
+ if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return false;
24470
+ this.cancelledTasks.delete(taskId);
24471
+ this.retryMap.delete(taskId);
24472
+ return this.opts.tracker.removeNode(taskId);
24473
+ }
24474
+ /**
24475
+ * Split a task into sub-tasks and delegate them to separate workers. The new
24476
+ * leaves inherit the parent's blockers (so they don't start before the
24477
+ * parent's dependencies are met), every existing dependent is rewired to
24478
+ * depend on ALL leaves (so downstream work waits for the whole split), and the
24479
+ * parent becomes a `completed` container. Refuses a running task (cancel it
24480
+ * first) or empty subtask list. Returns the new leaf ids (empty on refusal).
24481
+ * The scheduler picks the new pending leaves up on its next dispatch pass.
24482
+ */
24483
+ splitTask(taskId, subtasks) {
24484
+ const tracker = this.opts.tracker;
24485
+ const node = tracker.getNode(taskId);
24486
+ if (!node) return [];
24487
+ if (node.status === "in_progress" || this.taskSubagents.has(taskId)) return [];
24488
+ if (!subtasks.length) return [];
24489
+ const blockers = tracker.getBlockers(taskId);
24490
+ const dependents = tracker.getDependents(taskId);
24491
+ const leafIds = subtasks.map(
24492
+ (s) => tracker.addNode({
24493
+ title: s.title,
24494
+ description: s.description,
24495
+ type: s.type ?? node.type,
24496
+ priority: s.priority ?? node.priority,
24497
+ status: "pending",
24498
+ parentId: taskId
24499
+ }).id
24500
+ );
24501
+ for (const leaf of leafIds) {
24502
+ for (const b of blockers) tracker.addDependency(b, leaf);
24503
+ for (const dep of dependents) tracker.addDependency(leaf, dep);
24504
+ }
24505
+ this.retryMap.delete(taskId);
24506
+ this.persistRetries(taskId, 0);
24507
+ tracker.updateNodeStatus(taskId, "completed", `split into ${leafIds.length} subtasks`);
24508
+ this.emit("sdd.task.split", { runId: this.runId, taskId, subtaskIds: leafIds });
24509
+ return leafIds;
24510
+ }
24511
+ async waitWhilePaused() {
24512
+ while (this.paused && !this.stopRequested) {
24513
+ await new Promise((r) => setTimeout(r, 100));
24514
+ }
24515
+ }
24516
+ /**
24517
+ * Continuous dependency-driven execution. Unlike a wave-barrier loop (where a
24518
+ * whole batch must finish before the next starts), this fills free worker
24519
+ * slots the instant a task's dependencies are satisfied: a fast task's
24520
+ * dependent starts immediately rather than waiting for a slow sibling. Truly
24521
+ * independent tasks run in parallel; dependency chains run in order. Returns
24522
+ * the final summary when the graph settles, deadlocks, stops, or hits a backstop.
24523
+ */
24524
+ async run() {
24525
+ this.stopRequested = false;
24526
+ this.restoreRetryMap();
24527
+ const startTime = Date.now();
24528
+ this.round = 0;
24529
+ this.dispatchSeq = 0;
24530
+ let totalDispatched = 0;
24531
+ this.buildCoordinator();
24532
+ if (this.opts.worktrees && !this.baseBranch) {
24533
+ const base = await this.opts.worktrees.currentBase().catch(() => null);
24534
+ if (base) this.baseBranch = base.branch;
24535
+ }
24536
+ this.emit("sdd.run.started", {
24537
+ runId: this.runId,
24538
+ graphId: this.opts.graph.id,
24539
+ specId: this.opts.graph.specId,
24540
+ total: this.opts.graph.nodes.size,
24541
+ baseBranch: this.baseBranch
24542
+ });
24543
+ this.recoveryRounds = 0;
24544
+ this.failedSweeps = 0;
24545
+ this.lastSweepCompleted = 0;
24546
+ let deadlocked = false;
24547
+ const running = /* @__PURE__ */ new Map();
24548
+ const dispatch = (task) => {
24549
+ totalDispatched++;
24550
+ const tracked = (async () => {
24551
+ try {
24552
+ return await this.executeOne(task);
24553
+ } catch (err) {
24554
+ this.opts.tracker.updateNodeStatus(task.id, "failed", `dispatch error: ${String(err)}`);
24555
+ this.emit("sdd.task.failed", { runId: this.runId, taskId: task.id, subagentId: "", error: String(err) });
24556
+ return { taskId: task.id, success: false };
24557
+ } finally {
24558
+ running.delete(task.id);
24559
+ }
24560
+ })();
24561
+ running.set(task.id, tracked);
24562
+ };
24563
+ while (!this.stopRequested) {
24564
+ if (totalDispatched >= this.maxTotalWaves) break;
24565
+ if (this.maxWallClockMs && Date.now() - startTime >= this.maxWallClockMs) break;
24566
+ await this.waitWhilePaused();
24567
+ if (this.stopRequested) break;
24568
+ let dispatchedThisRound = 0;
24569
+ if (running.size < this.slots) {
24570
+ const ready = this.decomposer.readyNodes().filter((t) => !running.has(t.id));
24571
+ for (const task of ready) {
24572
+ if (running.size >= this.slots) break;
24573
+ dispatch(task);
24574
+ dispatchedThisRound++;
24575
+ }
24576
+ }
24577
+ if (dispatchedThisRound > 0) {
24578
+ this.emit("sdd.wave", { runId: this.runId, wave: this.round, batchSize: dispatchedThisRound });
24579
+ this.round++;
24580
+ }
24581
+ if (running.size === 0) {
24582
+ if (this.decomposer.isSettled()) {
24583
+ const completed = this.opts.tracker.getProgress().completed;
24584
+ const madeProgress = this.failedSweeps === 0 || completed > this.lastSweepCompleted;
24585
+ if (this.failedSweeps < this.maxFailedSweeps && madeProgress && this.requeueFailedTasks() > 0) {
24586
+ this.lastSweepCompleted = completed;
24587
+ this.failedSweeps++;
24588
+ continue;
24589
+ }
24590
+ break;
24591
+ }
24592
+ const chains = this.computeDeadlockChains();
24593
+ if (chains.length > 0) {
24594
+ this.emit("sdd.deadlock", { runId: this.runId, chains });
24595
+ if (this.recoveryRounds < this.maxRecoveryRounds && this.recoverFailedBlockers()) {
24596
+ this.recoveryRounds++;
24597
+ continue;
24598
+ }
24599
+ deadlocked = true;
24600
+ }
24601
+ break;
24602
+ }
24603
+ const moreReadyNow = running.size < this.slots && this.decomposer.readyNodes().some((t) => !running.has(t.id));
24604
+ if (!moreReadyNow) {
24605
+ await Promise.race(running.values());
24606
+ this.opts.onProgress?.(this.buildProgress());
24607
+ }
24608
+ }
24609
+ if (running.size > 0) await Promise.allSettled(running.values());
24610
+ if (this.stopRequested) await this.teardown();
24611
+ const finalProgress = this.opts.tracker.getProgress();
24612
+ this.emit("sdd.run.finished", {
24613
+ runId: this.runId,
24614
+ deadlocked,
24615
+ completed: finalProgress.completed,
24616
+ failed: finalProgress.failed,
24617
+ stopped: this.stopRequested
24618
+ });
24619
+ return {
24620
+ totalWaves: this.round,
24621
+ totalCompleted: finalProgress.completed,
24622
+ totalFailed: finalProgress.failed,
24623
+ totalDurationMs: Date.now() - startTime,
24624
+ deadlocked,
24625
+ stopRequested: this.stopRequested,
24626
+ finalProgress
24627
+ };
24628
+ }
24629
+ /**
24630
+ * Compute the blocking chains for a deadlock: every still-incomplete task and
24631
+ * the blockers (by node id) that are NOT completed. Failed blockers are
24632
+ * included since they're the usual deadlock cause once retries are exhausted.
24633
+ */
24634
+ computeDeadlockChains() {
24635
+ const tracker = this.opts.tracker;
24636
+ const chains = [];
24637
+ for (const node of tracker.getAllNodes()) {
24638
+ if (node.status === "completed" || node.status === "failed") continue;
24639
+ const blockedBy = tracker.getBlockers(node.id).filter((id) => tracker.getNode(id)?.status !== "completed");
24640
+ if (blockedBy.length > 0) chains.push({ blocked: node.id, blockedBy });
24641
+ }
24642
+ return chains;
24643
+ }
24644
+ /** Requeue failed tasks that block an incomplete dependent. Returns true if any. */
24645
+ recoverFailedBlockers() {
24646
+ const tracker = this.opts.tracker;
24647
+ let recovered = false;
24648
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
24649
+ const blocksIncomplete = tracker.getDependents(node.id).some((d) => {
24650
+ const s = tracker.getNode(d)?.status;
24651
+ return s !== "completed" && s !== "failed";
24652
+ });
24653
+ if (blocksIncomplete) {
24654
+ this.retryMap.delete(node.id);
24655
+ this.persistRetries(node.id, 0);
24656
+ tracker.updateNodeStatus(node.id, "pending", "deadlock recovery");
24657
+ recovered = true;
24658
+ }
24659
+ }
24660
+ return recovered;
24661
+ }
24662
+ /**
24663
+ * Requeue every terminal-failed task that the user did NOT cancel, giving each
24664
+ * a fresh `maxRetries` budget. Shared by the automatic end-of-run sweep and
24665
+ * the manual "retry all failed" control. Returns the number requeued.
24666
+ */
24667
+ requeueFailedTasks(reason = "retry failed sweep") {
24668
+ const tracker = this.opts.tracker;
24669
+ let n = 0;
24670
+ for (const node of tracker.getAllNodes({ status: ["failed"] })) {
24671
+ if (this.cancelledTasks.has(node.id) || node.metadata?.cancelled) continue;
24672
+ this.retryMap.delete(node.id);
24673
+ this.persistRetries(node.id, 0);
24674
+ tracker.updateNodeStatus(node.id, "pending", reason);
24675
+ this.emit("sdd.task.retrying", {
24676
+ runId: this.runId,
24677
+ taskId: node.id,
24678
+ attempt: 0,
24679
+ maxRetries: this.maxRetries
24680
+ });
24681
+ n++;
24682
+ }
24683
+ return n;
24684
+ }
24685
+ /**
24686
+ * Manually requeue all failed tasks to `pending` (board "Retry all failed").
24687
+ * Unlike the automatic sweep this also clears any `cancelled` marker, so a
24688
+ * user can bring cancelled tasks back in the same action — mirroring
24689
+ * `retryTask`. Picked up by the running scheduler on its next dispatch pass.
24690
+ * Returns the number of tasks requeued.
24691
+ */
24692
+ retryAllFailed() {
24693
+ const failed = this.opts.tracker.getAllNodes({ status: ["failed"] });
24694
+ for (const node of failed) {
24695
+ this.cancelledTasks.delete(node.id);
24696
+ this.opts.tracker.patchMetadata(node.id, { cancelled: void 0 });
24697
+ }
24698
+ return this.requeueFailedTasks("manual retry all");
24699
+ }
24700
+ /** Restore per-task retry counts persisted in node metadata (resume support). */
24701
+ restoreRetryMap() {
24702
+ this.retryMap.clear();
24703
+ for (const node of this.opts.tracker.getAllNodes()) {
24704
+ const r = node.metadata?.retries;
24705
+ if (typeof r === "number" && r > 0) this.retryMap.set(node.id, r);
24706
+ }
24707
+ }
24708
+ /**
24709
+ * Reset orphaned `in_progress` tasks (no agent runs them after a crash) back
24710
+ * to `pending` so a fresh run re-executes them. Call before constructing a run
24711
+ * from a reloaded graph. Static so callers don't need a run instance.
24712
+ */
24713
+ static resetOrphans(tracker) {
24714
+ let n = 0;
24715
+ for (const node of tracker.getAllNodes({ status: ["in_progress"] })) {
24716
+ tracker.updateNodeStatus(node.id, "pending", "resume: orphaned in_progress");
24717
+ n++;
24718
+ }
24719
+ return n;
24720
+ }
24721
+ /** Clean teardown after a stop: reset interrupted tasks + release worktrees. */
24722
+ async teardown() {
24723
+ for (const node of this.opts.tracker.getAllNodes({ status: ["in_progress"] })) {
24724
+ this.opts.tracker.updateNodeStatus(node.id, "pending", "run stopped");
24725
+ }
24726
+ const wt = this.opts.worktrees;
24727
+ if (wt) {
24728
+ for (const [taskId, handle] of [...this.taskWorktrees]) {
24729
+ await wt.release(handle, { keep: true }).catch(() => {
24730
+ });
24731
+ this.forgetWorktree(taskId);
24732
+ }
24733
+ }
24734
+ }
24735
+ // -------------------------------------------------------------------
24736
+ // Internal
24737
+ // -------------------------------------------------------------------
24738
+ buildCoordinator() {
24739
+ const config = {
24740
+ coordinatorId: `sdd-parallel-${randomUUID().slice(0, 8)}`,
24741
+ maxConcurrent: this.slots,
24742
+ doneCondition: { type: "all_tasks_done" },
24743
+ // Default budget guard for every spawned worker: idle reaper (resets on
24744
+ // activity) plus the opt-in wall-clock cap when one was configured. This
24745
+ // ensures the reaper applies even if a per-spawn config path is bypassed.
24746
+ defaultBudget: {
24747
+ idleTimeoutMs: this.idleTimeoutMs,
24748
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
24749
+ }
23624
24750
  };
23625
24751
  this.coordinator = new DefaultMultiAgentCoordinator(config);
23626
24752
  const baseFactory = this.opts.subagentFactory ?? this.defaultFactory();
@@ -23634,22 +24760,89 @@ var SddParallelRun = class {
23634
24760
  events: this.opts.agent.events
23635
24761
  });
23636
24762
  }
24763
+ /**
24764
+ * Execute a batch of tasks together. Retained as a thin wrapper over the
24765
+ * single-task primitive `executeOne` so the wave-oriented tests and any
24766
+ * batch callers keep working; the continuous scheduler in `run()` calls
24767
+ * `executeOne` directly. Throws if no coordinator is wired or a spawn fails
24768
+ * (surfaced from `executeOne`), preserving the original all-or-nothing contract.
24769
+ */
23637
24770
  async executeWave(batch) {
23638
- const wave = batch.wave;
23639
- const tasks = batch.tasks;
23640
24771
  const waveStart = Date.now();
23641
- for (const task of tasks) {
23642
- this.opts.tracker.updateNodeStatus(task.id, "in_progress");
24772
+ const outcomes = await Promise.all(batch.tasks.map((task) => this.executeOne(task)));
24773
+ const results = outcomes.map((o) => o.result).filter((r) => Boolean(r));
24774
+ const successCount = outcomes.filter((o) => o.success).length;
24775
+ const failCount = outcomes.length - successCount;
24776
+ return {
24777
+ wave: batch.wave,
24778
+ batch,
24779
+ results,
24780
+ successCount,
24781
+ failCount,
24782
+ durationMs: Date.now() - waveStart,
24783
+ stopRequested: this.stopRequested
24784
+ };
24785
+ }
24786
+ /**
24787
+ * Execute one task end-to-end: assign a worker identity, allocate its worktree,
24788
+ * spawn + assign the subagent, await its result, then update tracker status
24789
+ * (success / retry / terminal-fail / cancelled) and resolve the worktree. This
24790
+ * is the unit the continuous scheduler dispatches into a free slot. Throws on a
24791
+ * missing coordinator or failed spawn so callers can enforce all-or-nothing.
24792
+ */
24793
+ async executeOne(task) {
24794
+ const taskId = task.id;
24795
+ let agentName = task.assignee;
24796
+ if (!agentName) {
24797
+ const nick = assignNickname("executor", this.usedNicknames);
24798
+ this.usedNicknames.add(nick.key);
24799
+ agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
24800
+ this.opts.tracker.updateNode(taskId, { assignee: agentName });
24801
+ }
24802
+ this.opts.tracker.updateNodeStatus(taskId, "in_progress");
24803
+ await this.allocateWorktrees([task]);
24804
+ if (!this.coordinator) throw new SddError({
24805
+ message: "SDD parallel runner requires a coordinator",
24806
+ code: ERROR_CODES.SDD_INVALID_STATE
24807
+ });
24808
+ const coordinator = this.coordinator;
24809
+ const subagentId = `sdd-d${this.dispatchSeq++}`;
24810
+ const correlationId = randomUUID();
24811
+ const meta = task.metadata ?? {};
24812
+ const model = (typeof meta.model === "string" ? meta.model : void 0) ?? this.opts.defaultModel;
24813
+ const provider = (typeof meta.provider === "string" ? meta.provider : void 0) ?? this.opts.defaultProvider;
24814
+ const fallbackModels = Array.isArray(meta.fallbackModels) ? meta.fallbackModels : this.opts.fallbackModels;
24815
+ const spawnResult = await coordinator.spawn({
24816
+ id: subagentId,
24817
+ name: agentName ?? subagentId,
24818
+ role: "executor",
24819
+ // Idle reaper is always on; the hard wall-clock cap only when opted in.
24820
+ idleTimeoutMs: this.idleTimeoutMs,
24821
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {},
24822
+ cwd: this.taskCwds.get(taskId),
24823
+ disabledTools: ["delegate"],
24824
+ ...model ? { model } : {},
24825
+ ...provider ? { provider } : {},
24826
+ ...fallbackModels && fallbackModels.length ? { fallbackModels } : {}
24827
+ });
24828
+ if (!spawnResult.subagentId) {
24829
+ throw new SddError({
24830
+ message: "One or more subagent spawns failed",
24831
+ code: ERROR_CODES.SDD_INVALID_STATE
24832
+ });
23643
24833
  }
23644
- const progress = computeTaskProgress(this.opts.graph);
23645
- const taskIds = tasks.map(() => randomUUID());
23646
- const subagentIds = tasks.map((_, i) => `sdd-wave${wave}-${i}`);
24834
+ this.taskSubagents.set(taskId, subagentId);
24835
+ this.emit("sdd.task.started", {
24836
+ runId: this.runId,
24837
+ taskId,
24838
+ subagentId,
24839
+ agentName: agentName ?? "",
24840
+ worktreeBranch: this.taskBranches.get(taskId)
24841
+ });
23647
24842
  const directivePreamble = [
23648
24843
  "\u2550\u2550\u2550 SDD PARALLEL EXECUTION \u2550\u2550\u2550",
23649
24844
  "",
23650
- `Wave ${wave + 1} of ~${Math.ceil(progress.total / this.slots)}`,
23651
24845
  `Graph: ${this.opts.graph.title}`,
23652
- `Parallel slots: ${tasks.length}`,
23653
24846
  "",
23654
24847
  "\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
23655
24848
  "\u2022 Execute the assigned SDD task end-to-end using multiple tool calls.",
@@ -23657,91 +24850,297 @@ var SddParallelRun = class {
23657
24850
  "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
23658
24851
  "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
23659
24852
  ].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);
24853
+ await coordinator.assign({
24854
+ id: correlationId,
24855
+ description: [
24856
+ directivePreamble,
24857
+ "",
24858
+ `\u2500\u2500 TASK \u2500\u2500`,
24859
+ `[${task.priority.toUpperCase()}] ${task.title}`,
24860
+ "",
24861
+ task.description
24862
+ ].join("\n"),
24863
+ subagentId,
24864
+ ...this.timeoutMs ? { timeoutMs: this.timeoutMs } : {}
23697
24865
  });
23698
- await Promise.all(assignPromises);
23699
- let results;
24866
+ let result;
23700
24867
  try {
23701
- results = await coordinator.awaitTasks(taskIds);
24868
+ const got = await coordinator.awaitTasks([correlationId]);
24869
+ result = expectDefined(got[0]);
23702
24870
  } catch (err) {
23703
- results = taskIds.map((id) => ({
23704
- subagentId: "",
23705
- taskId: id,
24871
+ result = {
24872
+ subagentId,
24873
+ taskId: correlationId,
23706
24874
  status: "failed",
23707
24875
  error: { kind: "unknown", message: String(err), retryable: false },
23708
24876
  iterations: 0,
23709
24877
  toolCalls: 0,
23710
24878
  durationMs: 0
23711
- }));
24879
+ };
24880
+ }
24881
+ this.taskSubagents.delete(taskId);
24882
+ if (this.cancelledTasks.has(taskId)) {
24883
+ await this.resolveWorktrees([task]);
24884
+ return { taskId, success: false, result };
24885
+ }
24886
+ let verificationFailReason;
24887
+ if (result.status === "success" && this.opts.verifyTask) {
24888
+ const cwd = this.taskCwds.get(taskId) ?? this.opts.projectRoot;
24889
+ try {
24890
+ const verdict = await this.opts.verifyTask({ task, result, cwd });
24891
+ if (!verdict.ok) {
24892
+ verificationFailReason = `verification failed: ${verdict.reason ?? "acceptance criteria not met"}`;
24893
+ }
24894
+ } catch (err) {
24895
+ verificationFailReason = `verification error: ${String(err)}`;
24896
+ }
24897
+ if (verificationFailReason) {
24898
+ this.emit("sdd.task.verification_failed", {
24899
+ runId: this.runId,
24900
+ taskId,
24901
+ reason: verificationFailReason
24902
+ });
24903
+ }
23712
24904
  }
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") {
24905
+ let success = false;
24906
+ if (result.status === "success" && !verificationFailReason) {
24907
+ const merged = await this.integrateWorktree(task, result);
24908
+ if (merged.ok) {
24909
+ success = true;
23719
24910
  this.opts.tracker.updateNodeStatus(taskId, "completed");
23720
24911
  this.retryMap.delete(taskId);
24912
+ this.persistRetries(taskId, 0);
24913
+ this.emit("sdd.task.completed", {
24914
+ runId: this.runId,
24915
+ taskId,
24916
+ subagentId,
24917
+ durationMs: result.durationMs
24918
+ });
24919
+ } else if (merged.reason) {
24920
+ this.emit("sdd.task.verification_failed", {
24921
+ runId: this.runId,
24922
+ taskId,
24923
+ reason: merged.reason
24924
+ });
24925
+ await this.applyTaskFailure(taskId, subagentId, merged.reason);
23721
24926
  } 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
- );
24927
+ this.emit("sdd.task.conflict", {
24928
+ runId: this.runId,
24929
+ taskId,
24930
+ conflictFiles: merged.conflictFiles ?? []
24931
+ });
24932
+ const reason = `merge conflict${merged.conflictFiles?.length ? `: ${merged.conflictFiles.join(", ")}` : ""}`;
24933
+ await this.applyTaskFailure(taskId, subagentId, reason);
24934
+ }
24935
+ } else {
24936
+ const errMsg = verificationFailReason ?? (result.error?.kind ? `${result.error.kind}: ${result.error.message}` : result.error?.message ?? "unknown error");
24937
+ await this.applyTaskFailure(taskId, subagentId, errMsg);
24938
+ await this.resolveWorktrees([task]);
24939
+ }
24940
+ return { taskId, success, result };
24941
+ }
24942
+ /**
24943
+ * Apply a task failure: retry (→ pending, bump retry count) while attempts
24944
+ * remain, else consult the optional supervisor (which can rescue via
24945
+ * retry/reassign/split), else terminal-fail (→ failed). Shared by the
24946
+ * worker-failure, verification-gate, and merge-conflict paths so all three
24947
+ * negotiate the same retry budget and emit the same events.
24948
+ */
24949
+ async applyTaskFailure(taskId, subagentId, errMsg) {
24950
+ const currentRetries = this.retryMap.get(taskId) ?? 0;
24951
+ if (currentRetries < this.maxRetries) {
24952
+ this.retryMap.set(taskId, currentRetries + 1);
24953
+ this.persistRetries(taskId, currentRetries + 1);
24954
+ this.opts.tracker.updateNodeStatus(
24955
+ taskId,
24956
+ "pending",
24957
+ `Retry ${currentRetries + 1}/${this.maxRetries}: ${errMsg}`
24958
+ );
24959
+ this.emit("sdd.task.retrying", {
24960
+ runId: this.runId,
24961
+ taskId,
24962
+ attempt: currentRetries + 1,
24963
+ maxRetries: this.maxRetries
24964
+ });
24965
+ return;
24966
+ }
24967
+ if (await this.trySupervisorRescue(taskId, errMsg)) return;
24968
+ this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
24969
+ this.emit("sdd.task.failed", { runId: this.runId, taskId, subagentId, error: errMsg });
24970
+ }
24971
+ /**
24972
+ * Consult `superviseFailure` for a task that has exhausted its retries.
24973
+ * Applies the verdict (retry / reassign+retry / split) and returns true when
24974
+ * the task was rescued (caller must NOT terminal-fail it). Bounded per task by
24975
+ * `maxSupervisorEscalations` so an always-"retry" supervisor can't loop forever.
24976
+ */
24977
+ async trySupervisorRescue(taskId, errMsg) {
24978
+ const supervise = this.opts.superviseFailure;
24979
+ if (!supervise) return false;
24980
+ const used = this.supervisorEscalations.get(taskId) ?? 0;
24981
+ if (used >= this.maxSupervisorEscalations) return false;
24982
+ const node = this.opts.tracker.getNode(taskId);
24983
+ if (!node) return false;
24984
+ let verdict;
24985
+ try {
24986
+ verdict = await supervise({ task: node, error: errMsg, attempts: used });
24987
+ } catch {
24988
+ return false;
24989
+ }
24990
+ if (!verdict || verdict.action === "fail") return false;
24991
+ this.supervisorEscalations.set(taskId, used + 1);
24992
+ const requeue = (reason) => {
24993
+ this.retryMap.delete(taskId);
24994
+ this.persistRetries(taskId, 0);
24995
+ this.opts.tracker.updateNodeStatus(taskId, "pending", reason);
24996
+ };
24997
+ if (verdict.action === "reassign") {
24998
+ this.setTaskModel(taskId, verdict.model, verdict.provider);
24999
+ requeue(`supervisor reassign: ${verdict.model ?? "default"}`);
25000
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "reassign" });
25001
+ return true;
25002
+ }
25003
+ if (verdict.action === "split") {
25004
+ const ids = this.splitTask(taskId, verdict.subtasks);
25005
+ if (ids.length === 0) return false;
25006
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "split" });
25007
+ return true;
25008
+ }
25009
+ requeue("supervisor retry");
25010
+ this.emit("sdd.supervisor.decision", { runId: this.runId, taskId, action: "retry" });
25011
+ return true;
25012
+ }
25013
+ /**
25014
+ * Integrate a verified-successful task's worktree into the base branch.
25015
+ * Commits, squash-merges (optionally running `conflictResolver` first), and on
25016
+ * success releases the worktree. On an UNRESOLVED conflict it returns
25017
+ * `{ok:false}` with the conflicting files so the caller routes the task into
25018
+ * the failure path (a retry forks a fresh worktree off the now-advanced base,
25019
+ * which usually clears the conflict). No-op `{ok:true}` when worktrees are
25020
+ * disabled or none was allocated for this task. Never throws — a merge hiccup
25021
+ * degrades to a (retryable) failure rather than wedging the run.
25022
+ */
25023
+ async integrateWorktree(task, result) {
25024
+ const wt = this.opts.worktrees;
25025
+ if (!wt) return { ok: true };
25026
+ const handle = this.taskWorktrees.get(task.id);
25027
+ if (!handle) return { ok: true };
25028
+ try {
25029
+ await wt.commitAll(handle, `sdd(${task.title}): ${task.id}`);
25030
+ const baseShaBefore = await wt.baseHead(handle);
25031
+ const baseSha = this.opts.conflictResolver ? baseShaBefore : null;
25032
+ const res = await wt.merge(handle, {
25033
+ squash: true,
25034
+ ...this.opts.conflictResolver ? {
25035
+ resolve: (info) => this.opts.conflictResolver({ task, conflictFiles: info.conflictFiles, cwd: info.cwd })
25036
+ } : {}
25037
+ });
25038
+ if (res.ok) {
25039
+ if (res.resolved && this.opts.verifyTask && baseSha) {
25040
+ let regressed;
25041
+ try {
25042
+ const verdict = await this.opts.verifyTask({
25043
+ task,
25044
+ result: result ?? {},
25045
+ cwd: this.opts.projectRoot
25046
+ });
25047
+ if (!verdict.ok) regressed = verdict.reason ?? "verification failed after conflict resolution";
25048
+ } catch (err) {
25049
+ regressed = `verification error after conflict resolution: ${String(err)}`;
25050
+ }
25051
+ if (regressed) {
25052
+ await wt.revertBaseTo(handle, baseSha).catch(() => {
25053
+ });
25054
+ await wt.release(handle, { keep: false }).catch(() => {
25055
+ });
25056
+ this.forgetWorktree(task.id, { keepBranchLabel: true });
25057
+ return { ok: false, conflictFiles: [], reason: regressed };
25058
+ }
25059
+ }
25060
+ const baseShaAfter = await wt.baseHead(handle);
25061
+ if (baseShaAfter && baseShaAfter !== baseShaBefore) {
25062
+ this.mergedCommits.push({ taskId: task.id, sha: baseShaAfter, title: task.title });
25063
+ this.emit("sdd.task.merged", { runId: this.runId, taskId: task.id, sha: baseShaAfter });
25064
+ }
25065
+ await wt.release(handle, { keep: false });
25066
+ this.forgetWorktree(task.id);
25067
+ return { ok: true };
25068
+ }
25069
+ await wt.release(handle, { keep: false }).catch(() => {
25070
+ });
25071
+ this.forgetWorktree(task.id, { keepBranchLabel: true });
25072
+ return { ok: false, conflictFiles: res.conflictFiles ?? [] };
25073
+ } catch {
25074
+ this.forgetWorktree(task.id);
25075
+ return { ok: false, conflictFiles: [] };
25076
+ }
25077
+ }
25078
+ /** Allocate a fresh git worktree per task in the batch (no-op without a manager). */
25079
+ async allocateWorktrees(tasks) {
25080
+ const wt = this.opts.worktrees;
25081
+ if (!wt) return;
25082
+ for (const task of tasks) {
25083
+ if (this.taskWorktrees.has(task.id)) continue;
25084
+ try {
25085
+ const handle = await wt.allocate(`sdd-${task.id}`, {
25086
+ slugHint: task.title,
25087
+ ownerLabel: task.title
25088
+ });
25089
+ if (handle.status === "active") {
25090
+ this.taskWorktrees.set(task.id, handle);
25091
+ this.taskCwds.set(task.id, handle.dir);
25092
+ this.taskBranches.set(task.id, handle.branch);
25093
+ const node = this.opts.tracker.getNode(task.id);
25094
+ if (node) node.metadata = { ...node.metadata, worktreeBranch: handle.branch };
25095
+ }
25096
+ } catch {
25097
+ }
25098
+ }
25099
+ }
25100
+ /**
25101
+ * Resolve each task's worktree after its result is known. Serialized merges
25102
+ * (one at a time) keep the base branch consistent; the wave structure already
25103
+ * guarantees dependency order (a task's blockers merged in an earlier wave).
25104
+ */
25105
+ async resolveWorktrees(tasks) {
25106
+ const wt = this.opts.worktrees;
25107
+ if (!wt) return;
25108
+ for (const task of tasks) {
25109
+ const handle = this.taskWorktrees.get(task.id);
25110
+ if (!handle) continue;
25111
+ const node = this.opts.tracker.getNode(task.id);
25112
+ const status = node?.status;
25113
+ const cancelled = Boolean(node?.metadata?.cancelled);
25114
+ try {
25115
+ if (cancelled) {
25116
+ await wt.release(handle, { keep: false });
25117
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
25118
+ } else if (status === "completed") {
25119
+ await wt.commitAll(handle, `sdd(${task.title}): ${task.id}`);
25120
+ await wt.merge(handle, { squash: true });
25121
+ await wt.release(handle, { keep: false });
25122
+ this.forgetWorktree(task.id);
25123
+ } else if (status === "failed") {
25124
+ await wt.release(handle, { keep: false });
25125
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
23731
25126
  } else {
23732
- this.opts.tracker.updateNodeStatus(taskId, "failed", errMsg);
25127
+ await wt.release(handle, { keep: false });
25128
+ this.forgetWorktree(task.id, { keepBranchLabel: false });
23733
25129
  }
25130
+ } catch {
25131
+ this.forgetWorktree(task.id);
23734
25132
  }
23735
25133
  }
23736
- return {
23737
- wave,
23738
- batch,
23739
- results,
23740
- successCount,
23741
- failCount,
23742
- durationMs: Date.now() - waveStart,
23743
- stopRequested: this.stopRequested
23744
- };
25134
+ }
25135
+ forgetWorktree(taskId, opts = {}) {
25136
+ this.taskWorktrees.delete(taskId);
25137
+ this.taskCwds.delete(taskId);
25138
+ if (!opts.keepBranchLabel) this.taskBranches.delete(taskId);
25139
+ }
25140
+ /** Persist a task's retry count into node metadata (survives crash → resume). */
25141
+ persistRetries(taskId, retries) {
25142
+ const node = this.opts.tracker.getNode(taskId);
25143
+ if (node) node.metadata = { ...node.metadata, retries };
23745
25144
  }
23746
25145
  buildProgress() {
23747
25146
  const gp = this.opts.tracker.getProgress();
@@ -23760,6 +25159,1611 @@ var SddParallelRun = class {
23760
25159
  }
23761
25160
  };
23762
25161
 
25162
+ // src/core/fallback-model.ts
25163
+ function parseModelRef(ref) {
25164
+ const trimmed = ref.trim();
25165
+ const slash = trimmed.indexOf("/");
25166
+ if (slash !== -1) {
25167
+ return {
25168
+ provider: trimmed.slice(0, slash) || void 0,
25169
+ model: trimmed.slice(slash + 1).trim()
25170
+ };
25171
+ }
25172
+ const parts = trimmed.split(/\s+/);
25173
+ if (parts.length >= 2) {
25174
+ return { provider: parts[0], model: parts.slice(1).join(" ") };
25175
+ }
25176
+ return { model: trimmed };
25177
+ }
25178
+
25179
+ // src/sdd/sdd-supervisor.ts
25180
+ var SddSupervisor = class {
25181
+ constructor(opts) {
25182
+ this.opts = opts;
25183
+ }
25184
+ opts;
25185
+ /**
25186
+ * Bind this as `SddParallelRunOptions.superviseFailure`. Returns a verdict the
25187
+ * run applies, or `undefined`/`{action:'fail'}` to let the task terminal-fail.
25188
+ */
25189
+ superviseFailure = async (info) => {
25190
+ const { task, error, attempts } = info;
25191
+ const canReassign = (this.opts.reassignModels?.length ?? 0) > 0;
25192
+ const canSplit = Boolean(this.opts.generateSubtasks);
25193
+ const decision = await this.opts.brain.decide({
25194
+ id: `sdd-supervisor-${task.id}-${attempts}`,
25195
+ source: "system",
25196
+ question: `SDD task "${task.title}" exhausted its retries. How should the run proceed?`,
25197
+ context: `Error: ${error}
25198
+ Supervisor rescues already used: ${attempts}`,
25199
+ options: [
25200
+ { id: "retry", label: "Retry the task as-is", recommended: true },
25201
+ ...canReassign ? [{ id: "reassign", label: "Reassign to a different model" }] : [],
25202
+ ...canSplit ? [{ id: "split", label: "Split into smaller sub-tasks" }] : [],
25203
+ { id: "fail", label: "Give up and mark the task failed" }
25204
+ ],
25205
+ // Higher risk once we've already rescued it once — pushes a wired LLM/human
25206
+ // toward a decisive verdict instead of looping retries.
25207
+ risk: attempts >= 1 ? "high" : "medium",
25208
+ // `continue` → policy answers in place (bounded retry, LLM never runs).
25209
+ // `ask_human` → policy escalates so the autonomous LLM layer can actually
25210
+ // pick reassign/split (see requestLlmVerdict's safety contract).
25211
+ fallback: this.opts.requestLlmVerdict ? "ask_human" : "continue"
25212
+ });
25213
+ if (decision.type === "deny") return { action: "fail" };
25214
+ if (decision.type !== "answer") return { action: "retry" };
25215
+ const choice = decision.optionId ?? "retry";
25216
+ if (choice === "fail") return { action: "fail" };
25217
+ if (choice === "reassign" && canReassign) {
25218
+ const models = this.opts.reassignModels;
25219
+ const ref = models[attempts % models.length];
25220
+ const parsed = ref ? parseModelRef(ref) : void 0;
25221
+ return { action: "reassign", model: parsed?.model, provider: parsed?.provider };
25222
+ }
25223
+ if (choice === "split" && this.opts.generateSubtasks) {
25224
+ const subtasks = await this.opts.generateSubtasks({ task, error }).catch(() => []);
25225
+ return subtasks.length ? { action: "split", subtasks } : { action: "retry" };
25226
+ }
25227
+ return { action: "retry" };
25228
+ };
25229
+ };
25230
+ function makeCommandVerifier(options = {}) {
25231
+ const metadataKey = options.metadataKey ?? "verificationCommand";
25232
+ const timeoutMs = options.timeoutMs ?? 18e4;
25233
+ return async function verifyTask(info) {
25234
+ const cmd = info.task.metadata?.[metadataKey];
25235
+ if (typeof cmd !== "string" || !cmd.trim()) return { ok: true };
25236
+ return await new Promise((resolve8) => {
25237
+ const child = spawn(cmd, { cwd: info.cwd, shell: true, windowsHide: true, stdio: "ignore" });
25238
+ const timer = setTimeout(() => {
25239
+ child.kill();
25240
+ resolve8({ ok: false, reason: `verification timed out: ${cmd}` });
25241
+ }, timeoutMs);
25242
+ child.on("exit", (code) => {
25243
+ clearTimeout(timer);
25244
+ resolve8(
25245
+ code === 0 ? { ok: true } : { ok: false, reason: `verification failed (exit ${code}): ${cmd}` }
25246
+ );
25247
+ });
25248
+ child.on("error", (err) => {
25249
+ clearTimeout(timer);
25250
+ resolve8({ ok: false, reason: `verification spawn error: ${String(err)}` });
25251
+ });
25252
+ });
25253
+ };
25254
+ }
25255
+
25256
+ // src/sdd/decompose-task.ts
25257
+ var TASK_TYPES = /* @__PURE__ */ new Set(["feature", "bugfix", "refactor", "docs", "test", "chore"]);
25258
+ var PRIORITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
25259
+ function extractJsonArray(text) {
25260
+ const fence = text.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
25261
+ if (fence?.[1]) return fence[1].trim();
25262
+ const bare = text.match(/(\[[\s\S]*\])/);
25263
+ if (bare?.[1]) {
25264
+ try {
25265
+ if (Array.isArray(JSON.parse(bare[1]))) return bare[1];
25266
+ } catch {
25267
+ }
25268
+ }
25269
+ return null;
25270
+ }
25271
+ function buildPrompt(task, error, min, max) {
25272
+ return [
25273
+ "You are an engineering lead triaging a software task that FAILED after every",
25274
+ "automated retry was exhausted. Break it into smaller, independently-executable",
25275
+ `sub-tasks (between ${min} and ${max}) so separate workers can each tackle a`,
25276
+ "narrower slice. Each sub-task must be strictly smaller than the parent \u2014 never",
25277
+ "restate the whole task as one sub-task.",
25278
+ "",
25279
+ `Parent task title: ${task.title}`,
25280
+ `Parent description: ${task.description}`,
25281
+ `Failure / error: ${error || "(none recorded)"}`,
25282
+ "",
25283
+ "Respond with ONLY a JSON array (no prose) of objects with this shape:",
25284
+ '[{"title": "...", "description": "...", "type": "feature|bugfix|refactor|docs|test|chore", "priority": "critical|high|medium|low"}]',
25285
+ "`type` and `priority` are optional (they default to the parent's)."
25286
+ ].join("\n");
25287
+ }
25288
+ function makeLlmSubtaskGenerator(opts) {
25289
+ const min = Math.max(2, opts.minSubtasks ?? 2);
25290
+ const max = Math.max(min, opts.maxSubtasks ?? 4);
25291
+ return async function generateSubtasks(info) {
25292
+ let text;
25293
+ try {
25294
+ text = await opts.run(buildPrompt(info.task, info.error, min, max));
25295
+ } catch {
25296
+ return [];
25297
+ }
25298
+ const json = extractJsonArray(text ?? "");
25299
+ if (!json) return [];
25300
+ let raw;
25301
+ try {
25302
+ raw = JSON.parse(json);
25303
+ } catch {
25304
+ return [];
25305
+ }
25306
+ if (!Array.isArray(raw)) return [];
25307
+ const specs = [];
25308
+ for (const item of raw) {
25309
+ if (!item || typeof item !== "object") continue;
25310
+ const r = item;
25311
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
25312
+ const description = typeof r["description"] === "string" ? r["description"].trim() : "";
25313
+ if (!title || !description) continue;
25314
+ const type = TASK_TYPES.has(r["type"]) ? r["type"] : void 0;
25315
+ const priority = PRIORITIES.has(r["priority"]) ? r["priority"] : void 0;
25316
+ specs.push({ title, description, type, priority });
25317
+ if (specs.length >= max) break;
25318
+ }
25319
+ return specs.length >= min ? specs : [];
25320
+ };
25321
+ }
25322
+ var START = "<<<<<<<";
25323
+ var BASE = "|||||||";
25324
+ var SEP2 = "=======";
25325
+ var END = ">>>>>>>";
25326
+ function resolveConflictText(text, side) {
25327
+ const out = [];
25328
+ let state = "normal";
25329
+ for (const line of text.split("\n")) {
25330
+ const marker = line.slice(0, 7);
25331
+ if (state === "normal" && marker === START) {
25332
+ state = "ours";
25333
+ continue;
25334
+ }
25335
+ if (state !== "normal" && marker === BASE) {
25336
+ state = "base";
25337
+ continue;
25338
+ }
25339
+ if (state !== "normal" && marker === SEP2) {
25340
+ state = "theirs";
25341
+ continue;
25342
+ }
25343
+ if (state !== "normal" && marker === END) {
25344
+ state = "normal";
25345
+ continue;
25346
+ }
25347
+ if (state === "normal") out.push(line);
25348
+ else if (state === "ours" && side === "base") out.push(line);
25349
+ else if (state === "theirs" && side === "incoming") out.push(line);
25350
+ }
25351
+ return out.join("\n");
25352
+ }
25353
+ function hasConflictMarkers(text) {
25354
+ return text.split("\n").some((l) => {
25355
+ const m = l.slice(0, 7);
25356
+ return m === START || m === SEP2 || m === END || m === BASE;
25357
+ });
25358
+ }
25359
+ function makePreferSideConflictResolver(side) {
25360
+ return async function conflictResolver(info) {
25361
+ if (info.conflictFiles.length === 0) return false;
25362
+ for (const rel of info.conflictFiles) {
25363
+ const abs = isAbsolute(rel) ? rel : join(info.cwd, rel);
25364
+ let content;
25365
+ try {
25366
+ content = await readFile(abs, "utf8");
25367
+ } catch {
25368
+ return false;
25369
+ }
25370
+ const resolved = resolveConflictText(content, side);
25371
+ if (hasConflictMarkers(resolved)) return false;
25372
+ try {
25373
+ await writeFile(abs, resolved, "utf8");
25374
+ } catch {
25375
+ return false;
25376
+ }
25377
+ }
25378
+ return true;
25379
+ };
25380
+ }
25381
+ function unfence(text) {
25382
+ const m = text.match(/^[\s\S]*?```[^\n]*\n([\s\S]*?)\n```[\s\S]*$/);
25383
+ return m?.[1] !== void 0 ? m[1] : text.trim();
25384
+ }
25385
+ function nonMarkerLineCount(text) {
25386
+ return text.split("\n").filter((l) => {
25387
+ const m = l.slice(0, 7);
25388
+ return m !== START && m !== SEP2 && m !== END && m !== BASE;
25389
+ }).length;
25390
+ }
25391
+ function makeLlmConflictResolver(opts) {
25392
+ const minFraction = opts.minRetainedFraction ?? 0.5;
25393
+ return async function conflictResolver(info) {
25394
+ if (info.conflictFiles.length === 0) return false;
25395
+ for (const rel of info.conflictFiles) {
25396
+ const abs = isAbsolute(rel) ? rel : join(info.cwd, rel);
25397
+ let content;
25398
+ try {
25399
+ content = await readFile(abs, "utf8");
25400
+ } catch {
25401
+ return false;
25402
+ }
25403
+ if (!hasConflictMarkers(content)) continue;
25404
+ const prompt = [
25405
+ "You are resolving a git MERGE CONFLICT in a single file. Below is the",
25406
+ "full file with conflict markers (<<<<<<<, =======, >>>>>>>, and possibly",
25407
+ "||||||| for diff3). Combine both sides into the correct, complete file \u2014",
25408
+ "keep ALL non-conflicting content verbatim and reconcile each hunk sensibly.",
25409
+ "Return ONLY the fully resolved file contents (no conflict markers, no",
25410
+ "commentary), optionally wrapped in a single ``` code fence.",
25411
+ "",
25412
+ `File: ${rel}`,
25413
+ "--- BEGIN ---",
25414
+ content,
25415
+ "--- END ---"
25416
+ ].join("\n");
25417
+ let out;
25418
+ try {
25419
+ out = await opts.run(prompt);
25420
+ } catch {
25421
+ return false;
25422
+ }
25423
+ const resolved = unfence(out ?? "");
25424
+ if (!resolved.trim() || hasConflictMarkers(resolved)) return false;
25425
+ if (resolved.split("\n").length < Math.floor(nonMarkerLineCount(content) * minFraction)) {
25426
+ return false;
25427
+ }
25428
+ try {
25429
+ await writeFile(abs, resolved, "utf8");
25430
+ } catch {
25431
+ return false;
25432
+ }
25433
+ }
25434
+ return true;
25435
+ };
25436
+ }
25437
+
25438
+ // src/sdd/board-types.ts
25439
+ function shortIdMap(graph) {
25440
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
25441
+ const m = /* @__PURE__ */ new Map();
25442
+ nodes.forEach((n, i) => {
25443
+ m.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
25444
+ });
25445
+ return m;
25446
+ }
25447
+ function buildBoardTasks(graph) {
25448
+ const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
25449
+ const shortId = shortIdMap(graph);
25450
+ const blockers = /* @__PURE__ */ new Map();
25451
+ for (const n of nodes) blockers.set(n.id, []);
25452
+ for (const e of graph.edges) {
25453
+ if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
25454
+ }
25455
+ const statusOf = (id) => graph.nodes.get(id)?.status;
25456
+ const depthCache = /* @__PURE__ */ new Map();
25457
+ const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
25458
+ const cached = depthCache.get(id);
25459
+ if (cached !== void 0) return cached;
25460
+ if (seen.has(id)) return 0;
25461
+ seen.add(id);
25462
+ const deps = blockers.get(id) ?? [];
25463
+ const d = deps.length === 0 ? 0 : 1 + Math.max(...deps.map((b) => depthOf(b, seen)));
25464
+ depthCache.set(id, d);
25465
+ return d;
25466
+ };
25467
+ const toTask = (n) => {
25468
+ const deps = blockers.get(n.id) ?? [];
25469
+ const allDepsDone = deps.every((b) => statusOf(b) === "completed");
25470
+ const meta = n.metadata ?? {};
25471
+ const cancelled = Boolean(meta["cancelled"]);
25472
+ const displayStatus = cancelled ? "cancelled" : n.status === "pending" && deps.length > 0 && allDepsDone ? "queued" : n.status;
25473
+ return {
25474
+ id: n.id,
25475
+ shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
25476
+ title: n.title,
25477
+ description: n.description,
25478
+ status: n.status,
25479
+ displayStatus,
25480
+ priority: n.priority,
25481
+ type: n.type,
25482
+ deps: deps.map((b) => shortId.get(b) ?? b.slice(0, 6)),
25483
+ agentName: n.assignee,
25484
+ worktreeBranch: typeof meta["worktreeBranch"] === "string" ? meta["worktreeBranch"] : void 0,
25485
+ startedAt: n.startedAt,
25486
+ completedAt: n.completedAt,
25487
+ retries: typeof meta["retries"] === "number" ? meta["retries"] : 0,
25488
+ model: typeof meta["model"] === "string" ? meta["model"] : void 0,
25489
+ provider: typeof meta["provider"] === "string" ? meta["provider"] : void 0,
25490
+ fallbackModels: Array.isArray(meta["fallbackModels"]) ? meta["fallbackModels"] : void 0,
25491
+ verificationCommand: typeof meta["verificationCommand"] === "string" ? meta["verificationCommand"] : void 0
25492
+ };
25493
+ };
25494
+ const tasks = nodes.map(toTask);
25495
+ const byDepth = /* @__PURE__ */ new Map();
25496
+ for (const n of nodes) {
25497
+ const d = depthOf(n.id);
25498
+ if (!byDepth.has(d)) byDepth.set(d, []);
25499
+ byDepth.get(d)?.push(shortId.get(n.id) ?? n.id.slice(0, 6));
25500
+ }
25501
+ const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, taskIds: byDepth.get(d) ?? [] }));
25502
+ return { tasks, columns };
25503
+ }
25504
+ function buildBoardSnapshot(graph, run, now) {
25505
+ const { tasks, columns } = buildBoardTasks(graph);
25506
+ return {
25507
+ runId: run.runId,
25508
+ specId: run.specId,
25509
+ graphId: graph.id,
25510
+ title: graph.title,
25511
+ status: run.status,
25512
+ startedAt: run.startedAt,
25513
+ updatedAt: now,
25514
+ progress: computeTaskProgress(graph),
25515
+ wave: run.wave,
25516
+ tasks,
25517
+ columns,
25518
+ diagnostics: run.deadlockChains?.length ? { deadlockChains: run.deadlockChains } : void 0,
25519
+ defaultModel: run.defaultModel,
25520
+ defaultProvider: run.defaultProvider,
25521
+ fallbackModels: run.fallbackModels,
25522
+ baseBranch: run.baseBranch,
25523
+ mergedCommits: run.mergedCommits?.length ? run.mergedCommits : void 0
25524
+ };
25525
+ }
25526
+
25527
+ // src/sdd/sdd-board-store.ts
25528
+ init_atomic_write();
25529
+ var SddBoardStore = class {
25530
+ baseDir;
25531
+ indexPath;
25532
+ constructor(opts) {
25533
+ this.baseDir = opts.baseDir;
25534
+ this.indexPath = path4.join(this.baseDir, "_index.json");
25535
+ }
25536
+ snapshotPath(runId) {
25537
+ return path4.join(this.baseDir, `${this.safe(runId)}.json`);
25538
+ }
25539
+ eventsPath(runId) {
25540
+ return path4.join(this.baseDir, `${this.safe(runId)}.events.jsonl`);
25541
+ }
25542
+ controlPath(runId) {
25543
+ return path4.join(this.baseDir, `${this.safe(runId)}.control.jsonl`);
25544
+ }
25545
+ async saveSnapshot(snapshot) {
25546
+ await ensureDir(this.baseDir);
25547
+ await atomicWrite(this.snapshotPath(snapshot.runId), JSON.stringify(snapshot, null, 2), {
25548
+ mode: 384
25549
+ });
25550
+ await this.updateIndex(snapshot);
25551
+ }
25552
+ async load(runId) {
25553
+ try {
25554
+ const raw = await fsp2.readFile(this.snapshotPath(runId), "utf8");
25555
+ return JSON.parse(raw);
25556
+ } catch {
25557
+ return null;
25558
+ }
25559
+ }
25560
+ async list() {
25561
+ const index = await this.readIndex();
25562
+ return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
25563
+ }
25564
+ async loadLatestForSpec(specId) {
25565
+ const entry = (await this.list()).find((e) => e.specId === specId);
25566
+ return entry ? this.load(entry.runId) : null;
25567
+ }
25568
+ /** Append one line to the board's JSONL event log (best-effort, never throws). */
25569
+ async appendEvent(runId, event) {
25570
+ try {
25571
+ await ensureDir(this.baseDir);
25572
+ await fsp2.appendFile(this.eventsPath(runId), `${JSON.stringify(event)}
25573
+ `, { mode: 384 });
25574
+ } catch {
25575
+ }
25576
+ }
25577
+ /** Append a control command (used by readers to steer a CLI-owned run). */
25578
+ async appendControl(runId, command) {
25579
+ await ensureDir(this.baseDir);
25580
+ await fsp2.appendFile(this.controlPath(runId), `${JSON.stringify(command)}
25581
+ `, { mode: 384 });
25582
+ }
25583
+ /** Read + truncate the control queue (the run drains it). Returns parsed commands. */
25584
+ async drainControl(runId) {
25585
+ const p = this.controlPath(runId);
25586
+ let raw;
25587
+ try {
25588
+ raw = await fsp2.readFile(p, "utf8");
25589
+ } catch {
25590
+ return [];
25591
+ }
25592
+ try {
25593
+ await fsp2.writeFile(p, "", { mode: 384 });
25594
+ } catch {
25595
+ }
25596
+ return raw.split("\n").filter((l) => l.trim()).map((l) => {
25597
+ try {
25598
+ return JSON.parse(l);
25599
+ } catch {
25600
+ return null;
25601
+ }
25602
+ }).filter((c) => c !== null);
25603
+ }
25604
+ async delete(runId) {
25605
+ await Promise.allSettled([
25606
+ fsp2.unlink(this.snapshotPath(runId)),
25607
+ fsp2.unlink(this.eventsPath(runId)),
25608
+ fsp2.unlink(this.controlPath(runId))
25609
+ ]);
25610
+ await this.removeFromIndex(runId);
25611
+ }
25612
+ // ── internal ────────────────────────────────────────────────────────────
25613
+ safe(runId) {
25614
+ return runId.replace(/[^a-zA-Z0-9._-]/g, "_");
25615
+ }
25616
+ async readIndex() {
25617
+ try {
25618
+ const raw = await fsp2.readFile(this.indexPath, "utf8");
25619
+ const parsed = JSON.parse(raw);
25620
+ if (parsed?.version === 1) return parsed;
25621
+ } catch {
25622
+ }
25623
+ return { version: 1, entries: [] };
25624
+ }
25625
+ async updateIndex(snapshot) {
25626
+ const index = await this.readIndex();
25627
+ const entry = {
25628
+ runId: snapshot.runId,
25629
+ specId: snapshot.specId,
25630
+ title: snapshot.title,
25631
+ status: snapshot.status,
25632
+ total: snapshot.progress.total,
25633
+ completed: snapshot.progress.completed,
25634
+ updatedAt: snapshot.updatedAt
25635
+ };
25636
+ const idx = index.entries.findIndex((e) => e.runId === snapshot.runId);
25637
+ if (idx >= 0) index.entries[idx] = entry;
25638
+ else index.entries.push(entry);
25639
+ await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
25640
+ }
25641
+ async removeFromIndex(runId) {
25642
+ const index = await this.readIndex();
25643
+ index.entries = index.entries.filter((e) => e.runId !== runId);
25644
+ await atomicWrite(this.indexPath, JSON.stringify(index, null, 2), { mode: 384 });
25645
+ }
25646
+ };
25647
+
25648
+ // src/sdd/sdd-board-projector.ts
25649
+ var SddBoardProjector = class _SddBoardProjector {
25650
+ o;
25651
+ now;
25652
+ throttleMs;
25653
+ shortId;
25654
+ status = "idle";
25655
+ wave = 0;
25656
+ startedAt;
25657
+ deadlockChains = [];
25658
+ /** Live activity feed, most recent first (capped). */
25659
+ feed = [];
25660
+ static FEED_CAP = 60;
25661
+ finished = false;
25662
+ runDeadlocked = false;
25663
+ runStopped = false;
25664
+ /** Squash commits the run landed on the base branch (for post-run rollback). */
25665
+ mergedCommits = [];
25666
+ /** Base branch reported by the run at start (overrides the constructor option). */
25667
+ runBaseBranch;
25668
+ dirty = false;
25669
+ timer = null;
25670
+ unsubs = [];
25671
+ /** Tail of in-flight persistence, so callers can await a settled state. */
25672
+ lastSave = Promise.resolve();
25673
+ constructor(opts) {
25674
+ this.o = opts;
25675
+ this.now = opts.now ?? Date.now;
25676
+ this.throttleMs = opts.throttleMs ?? 250;
25677
+ this.shortId = shortIdMap(opts.graph);
25678
+ this.startedAt = this.now();
25679
+ this.unsubs.push(opts.tracker.subscribe(() => this.markDirty()));
25680
+ this.onRun("sdd.run.started", (e) => {
25681
+ this.status = "running";
25682
+ this.startedAt = this.now();
25683
+ if (e.baseBranch) this.runBaseBranch = e.baseBranch;
25684
+ this.markDirty();
25685
+ });
25686
+ this.onRun("sdd.run.finished", (e) => {
25687
+ this.finished = true;
25688
+ this.runDeadlocked = e.deadlocked;
25689
+ this.runStopped = e.stopped;
25690
+ this.flush();
25691
+ });
25692
+ this.onRun("sdd.wave", (e) => {
25693
+ this.wave = e.wave;
25694
+ this.pushFeed({ ts: this.now(), kind: "wave", text: `Wave ${e.wave + 1} started \xB7 ${e.batchSize} task(s) in parallel` });
25695
+ this.markDirty();
25696
+ });
25697
+ this.onRun("sdd.deadlock", (e) => {
25698
+ this.deadlockChains = e.chains.map((c) => ({
25699
+ blocked: this.shortId.get(c.blocked) ?? c.blocked.slice(0, 6),
25700
+ blockedBy: c.blockedBy.map((b) => this.shortId.get(b) ?? b.slice(0, 6))
25701
+ }));
25702
+ this.pushFeed({ ts: this.now(), kind: "deadlock", text: `Deadlock \u2014 ${e.chains.length} task(s) blocked by failed work` });
25703
+ this.markDirty();
25704
+ });
25705
+ this.onRun("sdd.task.started", (e) => {
25706
+ const sid = this.shortId.get(e.taskId);
25707
+ this.pushFeed({
25708
+ ts: this.now(),
25709
+ kind: "started",
25710
+ taskShortId: sid,
25711
+ agentName: e.agentName,
25712
+ text: `${e.agentName || "a worker"} picked up ${sid ?? "a task"}${this.titleOf(e.taskId)}`
25713
+ });
25714
+ this.markDirty();
25715
+ });
25716
+ this.onRun("sdd.task.completed", (e) => {
25717
+ const sid = this.shortId.get(e.taskId);
25718
+ const agent = this.assigneeOf(e.taskId);
25719
+ this.pushFeed({
25720
+ ts: this.now(),
25721
+ kind: "completed",
25722
+ taskShortId: sid,
25723
+ agentName: agent,
25724
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} completed${agent ? ` by ${agent}` : ""} \xB7 ${(e.durationMs / 1e3).toFixed(1)}s`
25725
+ });
25726
+ this.markDirty();
25727
+ });
25728
+ this.onRun("sdd.task.failed", (e) => {
25729
+ const sid = this.shortId.get(e.taskId);
25730
+ this.pushFeed({
25731
+ ts: this.now(),
25732
+ kind: "failed",
25733
+ taskShortId: sid,
25734
+ agentName: this.assigneeOf(e.taskId),
25735
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} failed \u2014 ${e.error}`
25736
+ });
25737
+ this.markDirty();
25738
+ });
25739
+ this.onRun("sdd.task.retrying", (e) => {
25740
+ const sid = this.shortId.get(e.taskId);
25741
+ this.pushFeed({
25742
+ ts: this.now(),
25743
+ kind: "retrying",
25744
+ taskShortId: sid,
25745
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} retrying (${e.attempt}/${e.maxRetries})`
25746
+ });
25747
+ this.markDirty();
25748
+ });
25749
+ this.onRun("sdd.task.verification_failed", (e) => {
25750
+ const sid = this.shortId.get(e.taskId);
25751
+ this.pushFeed({
25752
+ ts: this.now(),
25753
+ kind: "verification_failed",
25754
+ taskShortId: sid,
25755
+ agentName: this.assigneeOf(e.taskId),
25756
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} failed verification \u2014 ${e.reason}`
25757
+ });
25758
+ this.markDirty();
25759
+ });
25760
+ this.onRun("sdd.task.conflict", (e) => {
25761
+ const sid = this.shortId.get(e.taskId);
25762
+ const files = e.conflictFiles.length;
25763
+ this.pushFeed({
25764
+ ts: this.now(),
25765
+ kind: "conflict",
25766
+ taskShortId: sid,
25767
+ agentName: this.assigneeOf(e.taskId),
25768
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} merge conflict \u2014 ${files} file(s)${files ? `: ${e.conflictFiles.slice(0, 3).join(", ")}${files > 3 ? "\u2026" : ""}` : ""}`
25769
+ });
25770
+ this.markDirty();
25771
+ });
25772
+ this.onRun("sdd.task.merged", (e) => {
25773
+ const title = this.o.graph.nodes.get(e.taskId)?.title ?? "";
25774
+ this.mergedCommits.push({ taskId: e.taskId, sha: e.sha, title });
25775
+ const sid = this.shortId.get(e.taskId);
25776
+ this.pushFeed({
25777
+ ts: this.now(),
25778
+ kind: "completed",
25779
+ taskShortId: sid,
25780
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} merged \u2192 ${this.runBaseBranch ?? this.o.baseBranch ?? "base"} (${e.sha.slice(0, 8)})`
25781
+ });
25782
+ this.markDirty();
25783
+ });
25784
+ this.onRun("sdd.task.split", (e) => {
25785
+ const sid = this.shortId.get(e.taskId);
25786
+ this.pushFeed({
25787
+ ts: this.now(),
25788
+ kind: "split",
25789
+ taskShortId: sid,
25790
+ text: `${sid ?? "task"}${this.titleOf(e.taskId)} split into ${e.subtaskIds.length} sub-task(s)`
25791
+ });
25792
+ this.markDirty();
25793
+ });
25794
+ this.onRun("sdd.supervisor.decision", (e) => {
25795
+ const sid = this.shortId.get(e.taskId);
25796
+ this.pushFeed({
25797
+ ts: this.now(),
25798
+ kind: "supervisor",
25799
+ taskShortId: sid,
25800
+ text: `supervisor \u2192 ${e.action} for ${sid ?? "task"}${this.titleOf(e.taskId)}${e.rationale ? ` (${e.rationale})` : ""}`
25801
+ });
25802
+ this.markDirty();
25803
+ });
25804
+ }
25805
+ pushFeed(entry) {
25806
+ this.feed.unshift(entry);
25807
+ if (this.feed.length > _SddBoardProjector.FEED_CAP) this.feed.length = _SddBoardProjector.FEED_CAP;
25808
+ }
25809
+ /** ` (title…)` suffix for a feed line, or '' when the node/title is missing. */
25810
+ titleOf(taskId) {
25811
+ const t = this.o.graph.nodes.get(taskId)?.title;
25812
+ if (!t) return "";
25813
+ return ` (${t.length > 40 ? `${t.slice(0, 39)}\u2026` : t})`;
25814
+ }
25815
+ assigneeOf(taskId) {
25816
+ return this.o.graph.nodes.get(taskId)?.assignee;
25817
+ }
25818
+ /** Latest snapshot, built on demand (e.g. for a late-joining client). */
25819
+ snapshot() {
25820
+ return this.build();
25821
+ }
25822
+ /** Resolve once all in-flight snapshot persistence has settled. */
25823
+ async drain() {
25824
+ await this.lastSave;
25825
+ }
25826
+ /** Stop projecting and release subscriptions. */
25827
+ dispose() {
25828
+ if (this.timer) {
25829
+ clearTimeout(this.timer);
25830
+ this.timer = null;
25831
+ }
25832
+ for (const u of this.unsubs) u();
25833
+ this.unsubs.length = 0;
25834
+ }
25835
+ // ── internal ────────────────────────────────────────────────────────────
25836
+ /** Subscribe to a run event scoped to this run id; also append to JSONL. */
25837
+ onRun(event, handler) {
25838
+ const wrapped = (e) => {
25839
+ if (e.runId !== this.o.runId) return;
25840
+ void this.o.store?.appendEvent(this.o.runId, { ts: this.now(), type: event, payload: e });
25841
+ handler(e);
25842
+ };
25843
+ const off = this.o.events.on(event, wrapped);
25844
+ this.unsubs.push(off);
25845
+ }
25846
+ resolveStatus(completed, total) {
25847
+ if (!this.finished) return this.status;
25848
+ if (this.runDeadlocked) return "deadlocked";
25849
+ if (total > 0 && completed >= total) return "completed";
25850
+ if (this.runStopped) return "paused";
25851
+ return "failed";
25852
+ }
25853
+ build() {
25854
+ const snap = buildBoardSnapshot(
25855
+ this.o.graph,
25856
+ {
25857
+ runId: this.o.runId,
25858
+ specId: this.o.specId,
25859
+ status: "running",
25860
+ startedAt: this.startedAt,
25861
+ wave: this.wave,
25862
+ deadlockChains: this.deadlockChains,
25863
+ defaultModel: this.o.defaultModel,
25864
+ defaultProvider: this.o.defaultProvider,
25865
+ fallbackModels: this.o.fallbackModels,
25866
+ baseBranch: this.runBaseBranch ?? this.o.baseBranch,
25867
+ mergedCommits: this.mergedCommits
25868
+ },
25869
+ this.now()
25870
+ );
25871
+ snap.status = this.resolveStatus(snap.progress.completed, snap.progress.total);
25872
+ snap.feed = this.feed.slice(0, _SddBoardProjector.FEED_CAP);
25873
+ return snap;
25874
+ }
25875
+ markDirty() {
25876
+ this.dirty = true;
25877
+ if (this.timer || this.finished) return;
25878
+ this.timer = setTimeout(() => {
25879
+ this.timer = null;
25880
+ if (this.dirty) this.flush();
25881
+ }, this.throttleMs);
25882
+ }
25883
+ flush() {
25884
+ this.dirty = false;
25885
+ if (this.timer) {
25886
+ clearTimeout(this.timer);
25887
+ this.timer = null;
25888
+ }
25889
+ const snap = this.build();
25890
+ this.o.events.emit("sdd.board.snapshot", { runId: this.o.runId, snapshot: snap });
25891
+ if (this.o.store) {
25892
+ const store = this.o.store;
25893
+ this.lastSave = this.lastSave.then(() => store.saveSnapshot(snap)).catch(() => {
25894
+ });
25895
+ }
25896
+ }
25897
+ };
25898
+
25899
+ // src/sdd/sdd-run-registry.ts
25900
+ var SddRunRegistry = class {
25901
+ current = null;
25902
+ register(control) {
25903
+ this.current = control;
25904
+ }
25905
+ clear(runId) {
25906
+ if (this.current?.runId === runId) this.current = null;
25907
+ }
25908
+ getActive() {
25909
+ return this.current;
25910
+ }
25911
+ };
25912
+
25913
+ // src/sdd/sdd-interview-driver.ts
25914
+ var SddInterviewDriver = class {
25915
+ builder;
25916
+ o;
25917
+ minQuestions;
25918
+ maxQuestions;
25919
+ tracker = null;
25920
+ graph = null;
25921
+ constructor(opts) {
25922
+ this.o = opts;
25923
+ this.minQuestions = opts.minQuestions ?? 2;
25924
+ this.maxQuestions = opts.maxQuestions ?? 10;
25925
+ this.builder = new AISpecBuilder({
25926
+ store: opts.specStore,
25927
+ sessionPath: opts.sessionPath,
25928
+ projectContext: opts.projectContext,
25929
+ minQuestions: this.minQuestions,
25930
+ maxQuestions: this.maxQuestions
25931
+ });
25932
+ }
25933
+ /** Begin a fresh interview. Returns the first AI prompt (a question kickoff). */
25934
+ start(title, intent) {
25935
+ this.builder.startSession(title, intent);
25936
+ this.tracker = null;
25937
+ this.graph = null;
25938
+ return this.builder.getAIPrompt();
25939
+ }
25940
+ /**
25941
+ * Resume a previously-persisted interview from disk. Re-hydrates the task
25942
+ * graph too when one was already produced. Returns true if a session loaded.
25943
+ */
25944
+ async loadExisting() {
25945
+ const loaded = await this.builder.loadSession();
25946
+ if (!loaded) return false;
25947
+ const graphId = this.builder.getTaskGraphId();
25948
+ if (graphId) {
25949
+ const graph = await this.o.graphStore.load(graphId);
25950
+ if (graph) {
25951
+ this.graph = graph;
25952
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
25953
+ tracker.setGraph(graph);
25954
+ this.tracker = tracker;
25955
+ }
25956
+ }
25957
+ return true;
25958
+ }
25959
+ phase() {
25960
+ return this.builder.getPhase();
25961
+ }
25962
+ currentPrompt() {
25963
+ return this.builder.getAIPrompt();
25964
+ }
25965
+ getTracker() {
25966
+ return this.tracker;
25967
+ }
25968
+ getGraph() {
25969
+ return this.graph;
25970
+ }
25971
+ /** Record a Q/A pair (the agent asked `question`, the user replied `answer`). */
25972
+ submitAnswer(question, answer) {
25973
+ this.builder.addAnswer(question, answer);
25974
+ }
25975
+ /**
25976
+ * Feed the agent's text output back into the interview. Detects, in order:
25977
+ * 1. a Specification JSON → setSpec (phase → spec_review) + persist to SpecStore
25978
+ * 2. an implementation plan (implementation phase) → setImplementation
25979
+ * 3. a task JSON array → build + persist a TaskGraph
25980
+ * Each step is independent and best-effort; a malformed payload is ignored
25981
+ * rather than thrown, so a chatty agent turn never breaks the interview.
25982
+ */
25983
+ async ingestAgentOutput(text) {
25984
+ const result = {
25985
+ specDetected: false,
25986
+ implementationDetected: false,
25987
+ tasksDetected: false
25988
+ };
25989
+ if (!this.builder.getSession().spec) {
25990
+ const spec = this.builder.tryParseSpecFromOutput(text);
25991
+ if (spec) {
25992
+ this.builder.setSpec(spec);
25993
+ await this.persistSpec(spec);
25994
+ result.specDetected = true;
25995
+ }
25996
+ }
25997
+ if (this.builder.getPhase() === "implementation") {
25998
+ if (this.trySaveImplementationPlan(text)) result.implementationDetected = true;
25999
+ }
26000
+ const session = this.builder.getSession();
26001
+ if (session.spec) {
26002
+ const built = await this.tryBuildTasksFromOutput(text);
26003
+ if (built) {
26004
+ result.tasksDetected = true;
26005
+ result.graphId = built;
26006
+ }
26007
+ }
26008
+ return result;
26009
+ }
26010
+ /**
26011
+ * Advance to the next phase (mirrors `/sdd approve`). When moving into the
26012
+ * executing phase, guarantees a task graph exists — deterministically
26013
+ * generating one from the approved spec if the agent never emitted a valid
26014
+ * task array. Returns the new phase and its AI prompt.
26015
+ */
26016
+ async approve() {
26017
+ const phase = this.builder.approve();
26018
+ if (phase === "executing") {
26019
+ await this.ensureTaskGraph();
26020
+ }
26021
+ return { phase, prompt: this.builder.getAIPrompt() };
26022
+ }
26023
+ /**
26024
+ * Ensure a TaskGraph exists for the approved spec. If the agent already
26025
+ * produced one (via `ingestAgentOutput`), returns it; otherwise builds a
26026
+ * deterministic graph from the spec's requirements via TaskGenerator. This is
26027
+ * the robustness backstop: a run can always start, even if the model never
26028
+ * emitted a parseable task array.
26029
+ */
26030
+ async ensureTaskGraph() {
26031
+ if (this.graph) return this.graph;
26032
+ const spec = this.builder.getSession().spec;
26033
+ if (!spec) return null;
26034
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
26035
+ const generator = new TaskGenerator({
26036
+ taskTracker: tracker,
26037
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
26038
+ });
26039
+ const graph = await generator.generateFromSpec(spec);
26040
+ this.tracker = tracker;
26041
+ this.graph = graph;
26042
+ await this.persistGraph(graph);
26043
+ this.builder.setTaskGraphId(graph.id);
26044
+ await this.builder.saveSession();
26045
+ return graph;
26046
+ }
26047
+ snapshot() {
26048
+ const s = this.builder.getSession();
26049
+ const spec = s.spec;
26050
+ return {
26051
+ sessionId: s.id,
26052
+ phase: s.phase,
26053
+ title: s.title,
26054
+ questionCount: s.questionCount,
26055
+ minQuestions: this.minQuestions,
26056
+ maxQuestions: this.maxQuestions,
26057
+ answers: s.answers.map((a) => ({ question: a.question, answer: a.answer })),
26058
+ spec: spec ? {
26059
+ id: spec.id,
26060
+ title: spec.title,
26061
+ overview: spec.overview,
26062
+ requirements: spec.requirements.map((r) => ({
26063
+ priority: r.priority,
26064
+ description: r.description
26065
+ }))
26066
+ } : void 0,
26067
+ graphId: s.taskGraphId,
26068
+ taskCount: this.graph ? this.graph.nodes.size : 0,
26069
+ board: this.graph ? buildBoardTasks(this.graph) : void 0,
26070
+ prompt: this.builder.getAIPrompt()
26071
+ };
26072
+ }
26073
+ // ── internals ────────────────────────────────────────────────────────────
26074
+ async persistSpec(spec) {
26075
+ try {
26076
+ await this.o.specStore.save(spec);
26077
+ } catch {
26078
+ }
26079
+ }
26080
+ async persistGraph(graph) {
26081
+ try {
26082
+ await this.o.graphStore.save(graph);
26083
+ } catch {
26084
+ }
26085
+ }
26086
+ /**
26087
+ * Port of the CLI `trySaveImplementationPlan` operating on this driver's
26088
+ * builder. Captures the prose plan that precedes the task JSON block.
26089
+ */
26090
+ trySaveImplementationPlan(text) {
26091
+ const current = this.builder.getSession().implementation ?? "";
26092
+ const jsonStart = text.match(/```json\s*\[/);
26093
+ if (jsonStart?.index && jsonStart.index > 0) {
26094
+ const plan = text.substring(0, jsonStart.index).trim();
26095
+ if (plan.length > 50 && plan !== current && !isExplanatoryText(plan)) {
26096
+ this.builder.setImplementation(plan);
26097
+ return true;
26098
+ }
26099
+ }
26100
+ if (text.length > 100 && !text.includes("```json") && text.trim() !== current && !isExplanatoryText(text)) {
26101
+ this.builder.setImplementation(text.trim());
26102
+ return true;
26103
+ }
26104
+ return false;
26105
+ }
26106
+ /**
26107
+ * Port of the CLI `trySaveTasksFromAIOutput`: parse a task JSON array from the
26108
+ * agent output, build (or extend) the tracker + graph, persist to disk, and
26109
+ * link the graphId to the session. Returns the graphId on success.
26110
+ */
26111
+ async tryBuildTasksFromOutput(text) {
26112
+ const json = this.builder.extractJSONArray(text);
26113
+ if (!json) return void 0;
26114
+ let tasks;
26115
+ try {
26116
+ tasks = JSON.parse(json);
26117
+ } catch {
26118
+ return void 0;
26119
+ }
26120
+ const valid = tasks.filter(
26121
+ (t) => t && typeof t === "object" && typeof t.title === "string" && t.title.length > 0
26122
+ );
26123
+ if (valid.length === 0) return void 0;
26124
+ const spec = this.builder.getSession().spec;
26125
+ if (!spec) return void 0;
26126
+ if (!this.tracker || !this.graph) {
26127
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
26128
+ this.graph = await tracker.createGraph(spec.id, spec.title);
26129
+ this.tracker = tracker;
26130
+ }
26131
+ const refMap = /* @__PURE__ */ new Map();
26132
+ const created = [];
26133
+ valid.forEach((task, i) => {
26134
+ const node = addTaskToTracker(this.tracker, task);
26135
+ created.push({ nodeId: node.id, task });
26136
+ if (typeof task.id === "string" && task.id.trim()) {
26137
+ refMap.set(task.id.trim().toLowerCase(), node.id);
26138
+ }
26139
+ refMap.set(`t${i + 1}`, node.id);
26140
+ refMap.set(String(i + 1), node.id);
26141
+ refMap.set(normalizeTaskRef(String(task.title)), node.id);
26142
+ });
26143
+ for (const { nodeId, task } of created) {
26144
+ const deps = Array.isArray(task.dependsOn) ? task.dependsOn : [];
26145
+ for (const ref of deps) {
26146
+ const depId = refMap.get(normalizeTaskRef(String(ref)));
26147
+ if (depId && depId !== nodeId) this.tracker.addDependency(depId, nodeId);
26148
+ }
26149
+ }
26150
+ await this.persistGraph(this.graph);
26151
+ this.builder.setTaskGraphId(this.graph.id);
26152
+ await this.builder.saveSession();
26153
+ return this.graph.id;
26154
+ }
26155
+ };
26156
+ var TASK_TYPES2 = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
26157
+ var TASK_PRIORITIES = ["critical", "high", "medium", "low"];
26158
+ function normalizeTaskRef(ref) {
26159
+ return ref.trim().toLowerCase();
26160
+ }
26161
+ function addTaskToTracker(tracker, task) {
26162
+ return tracker.addNode({
26163
+ title: String(task.title),
26164
+ description: String(task.description ?? ""),
26165
+ type: TASK_TYPES2.includes(String(task.type)) ? String(task.type) : "feature",
26166
+ priority: TASK_PRIORITIES.includes(String(task.priority)) ? String(task.priority) : "medium",
26167
+ status: "pending",
26168
+ estimateHours: Number(task.estimateHours) || 2,
26169
+ tags: Array.isArray(task.tags) ? task.tags.map(String) : []
26170
+ });
26171
+ }
26172
+ function isExplanatoryText(text) {
26173
+ const lower = text.toLowerCase();
26174
+ 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(".");
26175
+ }
26176
+
26177
+ // src/sdd/start-sdd-run.ts
26178
+ function startSddRun(opts) {
26179
+ SddParallelRun.resetOrphans(opts.tracker);
26180
+ const run = new SddParallelRun({
26181
+ tracker: opts.tracker,
26182
+ graph: opts.graph,
26183
+ agent: opts.agent,
26184
+ projectRoot: opts.projectRoot,
26185
+ parallelSlots: opts.parallelSlots,
26186
+ taskTimeoutMs: opts.taskTimeoutMs,
26187
+ taskIdleTimeoutMs: opts.taskIdleTimeoutMs,
26188
+ maxFailedRetrySweeps: opts.maxFailedRetrySweeps,
26189
+ verifyTask: opts.verifyTask,
26190
+ conflictResolver: opts.conflictResolver,
26191
+ superviseFailure: opts.superviseFailure,
26192
+ subagentFactory: opts.subagentFactory,
26193
+ events: opts.events,
26194
+ worktrees: opts.worktrees,
26195
+ maxRecoveryRounds: opts.maxRecoveryRounds ?? 1,
26196
+ onProgress: opts.onProgress,
26197
+ defaultModel: opts.defaultModel,
26198
+ defaultProvider: opts.defaultProvider,
26199
+ fallbackModels: opts.fallbackModels
26200
+ });
26201
+ const projector = new SddBoardProjector({
26202
+ runId: run.runId,
26203
+ graph: opts.graph,
26204
+ tracker: opts.tracker,
26205
+ events: opts.events,
26206
+ store: opts.boardStore,
26207
+ specId: opts.graph.specId,
26208
+ defaultModel: opts.defaultModel,
26209
+ defaultProvider: opts.defaultProvider,
26210
+ fallbackModels: opts.fallbackModels
26211
+ });
26212
+ opts.registry?.register({
26213
+ runId: run.runId,
26214
+ specId: opts.graph.specId,
26215
+ pause: () => run.pause(),
26216
+ resume: () => run.resume(),
26217
+ stop: () => run.stop(),
26218
+ retryTask: (id) => run.retryTask(id),
26219
+ retryAllFailed: () => run.retryAllFailed(),
26220
+ reassignTask: (id, name) => run.reassignTask(id, name),
26221
+ setTaskModel: (id, model, provider) => run.setTaskModel(id, model, provider),
26222
+ setTaskFallbacks: (id, fb) => run.setTaskFallbacks(id, fb),
26223
+ setTaskVerification: (id, cmd) => run.setTaskVerification(id, cmd),
26224
+ cancelTask: (id) => run.cancelTask(id),
26225
+ deleteTask: (id) => run.deleteTask(id),
26226
+ splitTask: (id, subtasks) => run.splitTask(id, subtasks),
26227
+ cleanupWorktrees: () => run.cleanupWorktrees(),
26228
+ rollback: () => run.rollback(),
26229
+ getBaseBranch: () => run.getBaseBranch(),
26230
+ getMergedCommits: () => run.getMergedCommits(),
26231
+ snapshot: () => projector.snapshot(),
26232
+ isRunning: () => run.isRunning()
26233
+ });
26234
+ const drainMs = opts.controlDrainMs ?? 500;
26235
+ const controlTimer = setInterval(() => {
26236
+ void opts.boardStore.drainControl(run.runId).then((cmds) => {
26237
+ for (const c of cmds) {
26238
+ const p = c.payload ?? {};
26239
+ if (c.type === "pause") run.pause();
26240
+ else if (c.type === "resume") run.resume();
26241
+ else if (c.type === "stop") run.stop();
26242
+ else if (c.type === "retry" && p.taskId) run.retryTask(p.taskId);
26243
+ else if (c.type === "retry_all_failed") run.retryAllFailed();
26244
+ else if (c.type === "reassign" && p.taskId) run.reassignTask(p.taskId, p.agentName ?? "");
26245
+ else if (c.type === "set_task_model" && p.taskId) run.setTaskModel(p.taskId, p.model, p.provider);
26246
+ else if (c.type === "set_task_fallbacks" && p.taskId) run.setTaskFallbacks(p.taskId, p.fallbackModels);
26247
+ else if (c.type === "set_task_verification" && p.taskId)
26248
+ run.setTaskVerification(p.taskId, p.verificationCommand);
26249
+ else if (c.type === "cancel_task" && p.taskId) void run.cancelTask(p.taskId);
26250
+ else if (c.type === "delete_task" && p.taskId) run.deleteTask(p.taskId);
26251
+ else if (c.type === "split_task" && p.taskId && p.subtasks?.length) run.splitTask(p.taskId, p.subtasks);
26252
+ else if (c.type === "cleanup_worktrees") void run.cleanupWorktrees();
26253
+ else if (c.type === "rollback") void run.rollback();
26254
+ }
26255
+ });
26256
+ }, drainMs);
26257
+ controlTimer.unref?.();
26258
+ const completion = (async () => {
26259
+ try {
26260
+ return await run.run();
26261
+ } finally {
26262
+ clearInterval(controlTimer);
26263
+ await projector.drain().catch(() => {
26264
+ });
26265
+ projector.dispose();
26266
+ opts.registry?.clear(run.runId);
26267
+ }
26268
+ })();
26269
+ return {
26270
+ run,
26271
+ runId: run.runId,
26272
+ projector,
26273
+ completion,
26274
+ stop: () => run.stop()
26275
+ };
26276
+ }
26277
+ var MAX_SLUG = 40;
26278
+ var WorktreeManager = class {
26279
+ projectRoot;
26280
+ events;
26281
+ gitBin;
26282
+ runGit;
26283
+ /** Keyed by ownerId. */
26284
+ handles = /* @__PURE__ */ new Map();
26285
+ usedSlugs = /* @__PURE__ */ new Set();
26286
+ constructor(opts) {
26287
+ this.projectRoot = resolve(opts.projectRoot);
26288
+ this.events = opts.events;
26289
+ this.gitBin = opts.gitBin ?? "git";
26290
+ this.runGit = opts.run ?? ((args, cwd) => this.defaultRun(args, cwd));
26291
+ }
26292
+ /** Create a fresh worktree + branch forked from the current base branch. */
26293
+ async allocate(ownerId, opts = {}) {
26294
+ const existing = this.handles.get(ownerId);
26295
+ if (existing && (existing.status === "allocating" || existing.status === "active")) {
26296
+ return existing;
26297
+ }
26298
+ const slug = this.makeSlug(opts.slugHint ?? ownerId);
26299
+ const branch = `wstack/ap/${slug}`;
26300
+ const dir = join(this.worktreesRoot(), slug);
26301
+ const absDir = resolve(dir);
26302
+ const absRoot = resolve(this.projectRoot);
26303
+ if (!absDir.startsWith(absRoot + sep)) {
26304
+ throw new Error(`Worktree dir "${absDir}" resolves outside project root`);
26305
+ }
26306
+ const baseBranch = opts.baseBranch ?? await this.detectBaseBranch();
26307
+ const handle = {
26308
+ id: slug,
26309
+ ownerId,
26310
+ ownerLabel: opts.ownerLabel ?? opts.slugHint ?? ownerId,
26311
+ slug,
26312
+ dir,
26313
+ branch,
26314
+ baseBranch,
26315
+ status: "allocating",
26316
+ createdAt: Date.now(),
26317
+ updatedAt: Date.now(),
26318
+ insertions: 0,
26319
+ deletions: 0,
26320
+ files: 0
26321
+ };
26322
+ this.handles.set(ownerId, handle);
26323
+ try {
26324
+ await mkdir(this.worktreesRoot(), { recursive: true });
26325
+ const res = await this.runGit(
26326
+ ["worktree", "add", "-b", branch, dir, baseBranch],
26327
+ this.projectRoot
26328
+ );
26329
+ if (res.code !== 0) {
26330
+ return this.fail(handle, res.stderr || "git worktree add failed");
26331
+ }
26332
+ } catch (err) {
26333
+ return this.fail(handle, toErrorMessage(err));
26334
+ }
26335
+ this.setStatus(handle, "active");
26336
+ this.emit("worktree.allocated", {
26337
+ handleId: handle.id,
26338
+ ownerId: handle.ownerId,
26339
+ ownerLabel: handle.ownerLabel,
26340
+ slug: handle.slug,
26341
+ dir: handle.dir,
26342
+ branch: handle.branch,
26343
+ baseBranch: handle.baseBranch
26344
+ });
26345
+ return handle;
26346
+ }
26347
+ /** Stage everything and commit inside the worktree. */
26348
+ async commitAll(handle, message) {
26349
+ this.setStatus(handle, "committing");
26350
+ await this.runGit(["add", "-A"], handle.dir);
26351
+ const staged = await this.runGit(["diff", "--cached", "--quiet"], handle.dir);
26352
+ if (staged.code === 0) {
26353
+ this.emitCommitted(handle, false);
26354
+ return { committed: false };
26355
+ }
26356
+ const idArgs = await this.identityArgs(handle.dir);
26357
+ const committed = await this.runGit([...idArgs, "commit", "-m", message], handle.dir);
26358
+ if (committed.code !== 0) {
26359
+ this.fail(handle, committed.stderr || "git commit failed");
26360
+ return { committed: false };
26361
+ }
26362
+ const stats = await this.collectStats(handle.dir);
26363
+ handle.insertions = stats.insertions;
26364
+ handle.deletions = stats.deletions;
26365
+ handle.files = stats.files;
26366
+ handle.sha = stats.sha;
26367
+ handle.updatedAt = Date.now();
26368
+ this.emitCommitted(handle, true);
26369
+ return { committed: true };
26370
+ }
26371
+ /** Merge the worktree branch back into the base branch (squash by default). */
26372
+ async merge(handle, opts = {}) {
26373
+ const squash = opts.squash ?? true;
26374
+ this.setStatus(handle, "merging");
26375
+ const checkout = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
26376
+ if (checkout.code !== 0) {
26377
+ this.fail(handle, checkout.stderr || `checkout ${handle.baseBranch} failed`);
26378
+ return { ok: false, stderr: checkout.stderr };
26379
+ }
26380
+ const mergeArgs = squash ? ["merge", "--squash", handle.branch] : ["merge", "--no-ff", handle.branch];
26381
+ const merged = await this.runGit(mergeArgs, this.projectRoot);
26382
+ if (merged.code !== 0) {
26383
+ const fromOutput = parseConflictPaths(`${merged.stdout}
26384
+ ${merged.stderr}`);
26385
+ const fromIndex = await this.unmergedFiles();
26386
+ const conflictFiles = [.../* @__PURE__ */ new Set([...fromOutput, ...fromIndex])];
26387
+ if (opts.resolve) {
26388
+ const finalized = await this.tryResolveConflict(handle, conflictFiles, opts);
26389
+ if (finalized) return finalized;
26390
+ }
26391
+ await this.runGit(["reset", "--hard", "HEAD"], this.projectRoot);
26392
+ handle.conflictFiles = conflictFiles;
26393
+ this.setStatus(handle, "needs-review", { lastError: merged.stderr });
26394
+ this.emit("worktree.conflict", {
26395
+ handleId: handle.id,
26396
+ ownerId: handle.ownerId,
26397
+ branch: handle.branch,
26398
+ conflictFiles
26399
+ });
26400
+ return { ok: false, conflict: true, conflictFiles, stderr: merged.stderr };
26401
+ }
26402
+ if (squash) {
26403
+ const msg = opts.message ?? `merge ${handle.branch} (squash)`;
26404
+ const idArgs = await this.identityArgs(this.projectRoot);
26405
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
26406
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
26407
+ this.fail(handle, commit.stderr || "squash commit failed");
26408
+ return { ok: false, stderr: commit.stderr };
26409
+ }
26410
+ }
26411
+ this.setStatus(handle, "merged");
26412
+ this.emit("worktree.merged", {
26413
+ handleId: handle.id,
26414
+ ownerId: handle.ownerId,
26415
+ branch: handle.branch,
26416
+ baseBranch: handle.baseBranch,
26417
+ squash
26418
+ });
26419
+ return { ok: true };
26420
+ }
26421
+ /**
26422
+ * Current tip SHA of a handle's base branch (without checking it out). Capture
26423
+ * this before a merge so a regressed merge can be reverted to exactly this
26424
+ * commit — unambiguous even when a squash produced no diff. Returns null on
26425
+ * failure (caller then skips the revert).
26426
+ */
26427
+ async baseHead(handle) {
26428
+ const res = await this.runGit(["rev-parse", handle.baseBranch], this.projectRoot);
26429
+ const sha = res.stdout.trim();
26430
+ return res.code === 0 && sha ? sha : null;
26431
+ }
26432
+ /**
26433
+ * Hard-reset the base branch back to `sha` (a value previously returned by
26434
+ * {@link baseHead}). Used to undo a squash-merge whose integrated result failed
26435
+ * re-verification, so an auto-resolved-but-broken merge never sticks on base.
26436
+ * Safe because SDD merges are serialized — no other commit lands in between.
26437
+ */
26438
+ async revertBaseTo(handle, sha) {
26439
+ const co = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
26440
+ if (co.code !== 0) return false;
26441
+ const reset = await this.runGit(["reset", "--hard", sha], this.projectRoot);
26442
+ return reset.code === 0;
26443
+ }
26444
+ /**
26445
+ * Current base branch + tip SHA, captured WITHOUT a handle. The SDD run calls
26446
+ * this once at start so a later rollback knows which branch the run's squash
26447
+ * commits landed on. Returns null when not in a usable git state.
26448
+ */
26449
+ async currentBase() {
26450
+ const branch = await this.detectBaseBranch();
26451
+ const head = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
26452
+ const sha = head.stdout.trim();
26453
+ return head.code === 0 && sha ? { branch, sha } : null;
26454
+ }
26455
+ /**
26456
+ * Force-remove EVERY managed worktree + branch this project owns, without
26457
+ * relying on the in-memory `handles` map — so it works post-run (a fresh
26458
+ * manager can clean up a previous run's leftovers). Enumerates
26459
+ * `git worktree list --porcelain`, removes every checkout living under the
26460
+ * `.wrongstack/worktrees` root, deletes every `wstack/ap/*` branch, then prunes.
26461
+ * Returns the number of worktrees removed. Never throws — best-effort cleanup.
26462
+ */
26463
+ async cleanupAllManaged() {
26464
+ const root = resolve(this.worktreesRoot());
26465
+ let removed = 0;
26466
+ try {
26467
+ const listed = await this.runGit(["worktree", "list", "--porcelain"], this.projectRoot);
26468
+ for (const line of listed.stdout.split("\n")) {
26469
+ const m = line.match(/^worktree\s+(.+?)\s*$/);
26470
+ if (!m?.[1]) continue;
26471
+ const dir = resolve(m[1]);
26472
+ if (dir !== root && (dir === root || dir.startsWith(root + sep))) {
26473
+ const rm3 = await this.runGit(["worktree", "remove", "--force", dir], this.projectRoot);
26474
+ if (rm3.code === 0) removed++;
26475
+ }
26476
+ }
26477
+ } catch {
26478
+ }
26479
+ try {
26480
+ const branches = await this.runGit(
26481
+ ["branch", "--list", "--format=%(refname:short)", "wstack/ap/*"],
26482
+ this.projectRoot
26483
+ );
26484
+ for (const b of branches.stdout.split("\n").map((s) => s.trim()).filter(Boolean)) {
26485
+ await this.runGit(["branch", "-D", b], this.projectRoot);
26486
+ }
26487
+ } catch {
26488
+ }
26489
+ await this.runGit(["worktree", "prune"], this.projectRoot).catch(() => void 0);
26490
+ this.handles.clear();
26491
+ this.emit("worktree.released", {
26492
+ handleId: "cleanup-all",
26493
+ ownerId: "cleanup-all",
26494
+ branch: "wstack/ap/*",
26495
+ kept: false
26496
+ });
26497
+ return { removed };
26498
+ }
26499
+ /**
26500
+ * Undo a run's squash commits by reverting each (newest → oldest) on the base
26501
+ * branch — history-preserving, never a destructive reset. Refuses on a dirty
26502
+ * working tree (so uncommitted work is never clobbered) and aborts cleanly if a
26503
+ * revert conflicts, reporting which SHA. `shas` are the run commit SHAs in the
26504
+ * order they landed; this reverses them. Returns the count reverted.
26505
+ */
26506
+ async revertCommits(baseBranch, shas) {
26507
+ if (shas.length === 0) return { ok: true, reverted: 0, reason: "nothing to revert" };
26508
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
26509
+ if (status.stdout.trim().length > 0) {
26510
+ return { ok: false, reverted: 0, reason: "working tree has uncommitted changes \u2014 commit or stash first" };
26511
+ }
26512
+ const co = await this.runGit(["checkout", baseBranch], this.projectRoot);
26513
+ if (co.code !== 0) {
26514
+ return { ok: false, reverted: 0, reason: co.stderr || `checkout ${baseBranch} failed` };
26515
+ }
26516
+ const idArgs = await this.identityArgs(this.projectRoot);
26517
+ let reverted = 0;
26518
+ for (const sha of [...shas].reverse()) {
26519
+ const res = await this.runGit([...idArgs, "revert", "--no-edit", sha], this.projectRoot);
26520
+ if (res.code !== 0) {
26521
+ await this.runGit(["revert", "--abort"], this.projectRoot).catch(() => void 0);
26522
+ return {
26523
+ ok: false,
26524
+ reverted,
26525
+ reason: `revert of ${sha.slice(0, 8)} failed: ${(res.stderr || res.stdout).trim().split("\n")[0] ?? "conflict"}`
26526
+ };
26527
+ }
26528
+ reverted++;
26529
+ }
26530
+ return { ok: true, reverted };
26531
+ }
26532
+ /**
26533
+ * Run the caller-supplied resolver against a conflicted squash-merge, then
26534
+ * commit if it cleared every marker. Returns a successful `MergeResult` on a
26535
+ * clean resolution, or `null` to signal the caller should fall back to the
26536
+ * abort path. Never leaves the base tree committed-but-dirty: a partial or
26537
+ * failed resolution returns `null` and the caller hard-resets.
26538
+ */
26539
+ async tryResolveConflict(handle, conflictFiles, opts) {
26540
+ let resolved = false;
26541
+ try {
26542
+ resolved = opts.resolve ? await opts.resolve({ conflictFiles, cwd: this.projectRoot }) : false;
26543
+ } catch {
26544
+ resolved = false;
26545
+ }
26546
+ if (!resolved) return null;
26547
+ await this.runGit(["add", "-A"], this.projectRoot);
26548
+ if (await this.hasConflictMarkers()) return null;
26549
+ const idArgs = await this.identityArgs(this.projectRoot);
26550
+ const msg = opts.message ?? `merge ${handle.branch} (squash, conflict resolved)`;
26551
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
26552
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
26553
+ return null;
26554
+ }
26555
+ handle.conflictFiles = conflictFiles;
26556
+ this.setStatus(handle, "merged");
26557
+ this.emit("worktree.merged", {
26558
+ handleId: handle.id,
26559
+ ownerId: handle.ownerId,
26560
+ branch: handle.branch,
26561
+ baseBranch: handle.baseBranch,
26562
+ squash: true
26563
+ });
26564
+ return { ok: true, resolved: true, conflictFiles };
26565
+ }
26566
+ /**
26567
+ * True when staged content still carries conflict markers. `git diff --cached
26568
+ * --check` exits nonzero and prints a "leftover conflict marker" line for each
26569
+ * survivor; whitespace-only errors (also flagged by --check) are ignored so a
26570
+ * clean resolution with unrelated whitespace is not rejected.
26571
+ */
26572
+ async hasConflictMarkers() {
26573
+ const check = await this.runGit(["diff", "--cached", "--check"], this.projectRoot);
26574
+ if (check.code === 0) return false;
26575
+ return /conflict marker/i.test(`${check.stdout}
26576
+ ${check.stderr}`);
26577
+ }
26578
+ /**
26579
+ * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`)
26580
+ * are left on disk for inspection.
26581
+ */
26582
+ async release(handle, opts = {}) {
26583
+ const keep = opts.keep || handle.status === "needs-review" || handle.status === "failed";
26584
+ if (!keep) {
26585
+ await this.runGit(["worktree", "remove", "--force", handle.dir], this.projectRoot);
26586
+ await this.runGit(["branch", "-D", handle.branch], this.projectRoot);
26587
+ await this.runGit(["worktree", "prune"], this.projectRoot);
26588
+ this.handles.delete(handle.ownerId);
26589
+ }
26590
+ this.emit("worktree.released", {
26591
+ handleId: handle.id,
26592
+ ownerId: handle.ownerId,
26593
+ branch: handle.branch,
26594
+ kept: keep
26595
+ });
26596
+ }
26597
+ get(ownerId) {
26598
+ return this.handles.get(ownerId);
26599
+ }
26600
+ list() {
26601
+ return [...this.handles.values()];
26602
+ }
26603
+ // ── internals ────────────────────────────────────────────────────────────
26604
+ worktreesRoot() {
26605
+ return join(this.projectRoot, ".wrongstack", "worktrees");
26606
+ }
26607
+ async detectBaseBranch() {
26608
+ const head = await this.runGit(["rev-parse", "--abbrev-ref", "HEAD"], this.projectRoot);
26609
+ const name = head.stdout.trim();
26610
+ if (name && name !== "HEAD") return name;
26611
+ const sha = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
26612
+ return sha.stdout.trim() || "HEAD";
26613
+ }
26614
+ makeSlug(hint) {
26615
+ let base = hint.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+/, "").replace(/[-.]+$/, "").slice(0, MAX_SLUG).replace(/[-.]+$/, "");
26616
+ if (!base) base = "wt";
26617
+ let slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
26618
+ while (this.usedSlugs.has(slug)) slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
26619
+ this.usedSlugs.add(slug);
26620
+ return slug;
26621
+ }
26622
+ async collectStats(dir) {
26623
+ const sha = (await this.runGit(["rev-parse", "HEAD"], dir)).stdout.trim();
26624
+ const numstat = await this.runGit(["show", "--numstat", "--format=", "HEAD"], dir);
26625
+ let insertions = 0;
26626
+ let deletions = 0;
26627
+ let files = 0;
26628
+ for (const line of numstat.stdout.split("\n")) {
26629
+ const m = line.trim().match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
26630
+ if (!m) continue;
26631
+ files++;
26632
+ if (m[1] !== "-") insertions += Number(m[1]);
26633
+ if (m[2] !== "-") deletions += Number(m[2]);
26634
+ }
26635
+ return { insertions, deletions, files, sha };
26636
+ }
26637
+ /**
26638
+ * `git -c user.*` fallback so commits succeed on machines and CI runners
26639
+ * that have no global git identity configured. Returns `[]` when both
26640
+ * `user.name` and `user.email` are already set (the common case), so a real
26641
+ * user's identity is never overridden. The worktree branch commits are
26642
+ * squashed away on merge, so the fallback identity never reaches the base
26643
+ * branch history.
26644
+ */
26645
+ async identityArgs(cwd) {
26646
+ const name = (await this.runGit(["config", "user.name"], cwd)).stdout.trim();
26647
+ const email = (await this.runGit(["config", "user.email"], cwd)).stdout.trim();
26648
+ if (name && email) return [];
26649
+ return [
26650
+ "-c",
26651
+ `user.name=${name || "AutoPhase"}`,
26652
+ "-c",
26653
+ `user.email=${email || "autophase@agent.local"}`
26654
+ ];
26655
+ }
26656
+ async unmergedFiles() {
26657
+ const res = await this.runGit(["diff", "--name-only", "--diff-filter=U"], this.projectRoot);
26658
+ return res.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
26659
+ }
26660
+ emitCommitted(handle, committed) {
26661
+ this.emit("worktree.committed", {
26662
+ handleId: handle.id,
26663
+ ownerId: handle.ownerId,
26664
+ branch: handle.branch,
26665
+ committed,
26666
+ insertions: handle.insertions,
26667
+ deletions: handle.deletions,
26668
+ files: handle.files,
26669
+ sha: handle.sha
26670
+ });
26671
+ }
26672
+ fail(handle, error) {
26673
+ this.setStatus(handle, "failed", { lastError: error });
26674
+ this.emit("worktree.failed", {
26675
+ handleId: handle.id,
26676
+ ownerId: handle.ownerId,
26677
+ branch: handle.branch,
26678
+ error
26679
+ });
26680
+ return handle;
26681
+ }
26682
+ setStatus(handle, status, patch) {
26683
+ handle.status = status;
26684
+ handle.updatedAt = Date.now();
26685
+ if (patch) Object.assign(handle, patch);
26686
+ }
26687
+ emit(event, payload) {
26688
+ this.events?.emit(event, payload);
26689
+ }
26690
+ defaultRun(args, cwd) {
26691
+ return new Promise((res) => {
26692
+ let stdout = "";
26693
+ let stderr = "";
26694
+ const MAX_GIT_OUTPUT = 1e6;
26695
+ const child = spawn(this.gitBin, args, {
26696
+ cwd,
26697
+ env: buildChildEnv(),
26698
+ stdio: ["ignore", "pipe", "pipe"],
26699
+ signal: AbortSignal.timeout(3e4),
26700
+ windowsHide: true
26701
+ });
26702
+ child.stdout?.on("data", (c) => {
26703
+ if (stdout.length < MAX_GIT_OUTPUT) stdout += c.toString();
26704
+ });
26705
+ child.stderr?.on("data", (c) => {
26706
+ if (stderr.length < MAX_GIT_OUTPUT) stderr += c.toString();
26707
+ });
26708
+ child.on("error", (err) => res({ code: 1, stdout, stderr: err.message }));
26709
+ child.on("close", (code) => res({ code: code ?? 1, stdout, stderr }));
26710
+ });
26711
+ }
26712
+ };
26713
+ function parseConflictPaths(output) {
26714
+ const paths = /* @__PURE__ */ new Set();
26715
+ for (const line of output.split("\n")) {
26716
+ const m = line.match(/^CONFLICT \([^)]*\): Merge conflict in (.+?)\s*$/);
26717
+ if (m?.[1]) paths.add(m[1]);
26718
+ }
26719
+ return [...paths];
26720
+ }
26721
+
26722
+ // src/sdd/sdd-lifecycle.ts
26723
+ async function cleanupSddWorktrees(projectRoot) {
26724
+ const wt = new WorktreeManager({ projectRoot });
26725
+ return wt.cleanupAllManaged();
26726
+ }
26727
+ async function rollbackSddRunFromDisk(opts) {
26728
+ const store = new SddBoardStore({ baseDir: opts.boardsDir });
26729
+ const runId = opts.runId ?? (await store.list())[0]?.runId;
26730
+ if (!runId) return { ok: false, reverted: 0, reason: "no SDD board found to roll back" };
26731
+ const snap = await store.load(runId);
26732
+ if (!snap) return { ok: false, reverted: 0, reason: `board "${runId}" not found` };
26733
+ if (!snap.baseBranch) {
26734
+ return { ok: false, reverted: 0, reason: "this run did not record a base branch (no worktree run)" };
26735
+ }
26736
+ const shas = (snap.mergedCommits ?? []).map((c) => c.sha);
26737
+ if (shas.length === 0) {
26738
+ return { ok: false, reverted: 0, reason: "no merged commits recorded for this run" };
26739
+ }
26740
+ const wt = new WorktreeManager({ projectRoot: opts.projectRoot });
26741
+ return wt.revertCommits(snap.baseBranch, shas);
26742
+ }
26743
+ async function destroySddProject(opts) {
26744
+ const { removed } = await cleanupSddWorktrees(opts.projectRoot).catch(() => ({ removed: 0 }));
26745
+ const deleted = [];
26746
+ const rmDir = async (dir, label) => {
26747
+ try {
26748
+ await fsp2.rm(dir, { recursive: true, force: true });
26749
+ deleted.push(label);
26750
+ } catch {
26751
+ }
26752
+ };
26753
+ const rmFile = async (file, label) => {
26754
+ try {
26755
+ await fsp2.unlink(file);
26756
+ deleted.push(label);
26757
+ } catch {
26758
+ }
26759
+ };
26760
+ await rmFile(opts.paths.projectSddSession, "session");
26761
+ await rmDir(opts.paths.projectSpecs, "specs");
26762
+ await rmDir(opts.paths.projectTaskGraphs, "task-graphs");
26763
+ await rmDir(opts.paths.projectSddBoards, "boards");
26764
+ return { worktreesRemoved: removed, deleted };
26765
+ }
26766
+
23763
26767
  // src/observability/metrics.ts
23764
26768
  var RESERVOIR_SIZE = 1024;
23765
26769
  function labelKey(labels) {
@@ -23919,9 +26923,9 @@ var DefaultHealthRegistry = class {
23919
26923
  }
23920
26924
  async runOne(check) {
23921
26925
  let timer = null;
23922
- const timeout = new Promise((resolve7) => {
26926
+ const timeout = new Promise((resolve8) => {
23923
26927
  timer = setTimeout(
23924
- () => resolve7({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
26928
+ () => resolve8({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
23925
26929
  this.timeoutMs
23926
26930
  );
23927
26931
  });
@@ -24104,7 +27108,7 @@ async function startMetricsServer(opts) {
24104
27108
  const tls = opts.tls;
24105
27109
  const useHttps = !!(tls?.cert && tls?.key);
24106
27110
  const host = opts.host ?? "127.0.0.1";
24107
- const path22 = opts.path ?? "/metrics";
27111
+ const path23 = opts.path ?? "/metrics";
24108
27112
  const healthPath = opts.healthPath ?? "/healthz";
24109
27113
  const healthRegistry = opts.healthRegistry;
24110
27114
  const listener = (req, res) => {
@@ -24114,7 +27118,7 @@ async function startMetricsServer(opts) {
24114
27118
  return;
24115
27119
  }
24116
27120
  const url = req.url.split("?")[0];
24117
- if (url === path22) {
27121
+ if (url === path23) {
24118
27122
  let body;
24119
27123
  try {
24120
27124
  body = renderPrometheus(opts.sink.snapshot());
@@ -24160,14 +27164,14 @@ async function startMetricsServer(opts) {
24160
27164
  const { createServer } = await import('http');
24161
27165
  server = createServer(listener);
24162
27166
  }
24163
- await new Promise((resolve7, reject) => {
27167
+ await new Promise((resolve8, reject) => {
24164
27168
  const onError = (err) => {
24165
27169
  server.off("listening", onListening);
24166
27170
  reject(err);
24167
27171
  };
24168
27172
  const onListening = () => {
24169
27173
  server.off("error", onError);
24170
- resolve7();
27174
+ resolve8();
24171
27175
  };
24172
27176
  server.once("error", onError);
24173
27177
  server.once("listening", onListening);
@@ -24178,9 +27182,9 @@ async function startMetricsServer(opts) {
24178
27182
  const protocol = useHttps ? "https" : "http";
24179
27183
  return {
24180
27184
  port: boundPort,
24181
- url: `${protocol}://${host}:${boundPort}${path22}`,
24182
- close: () => new Promise((resolve7, reject) => {
24183
- server.close((err) => err ? reject(err) : resolve7());
27185
+ url: `${protocol}://${host}:${boundPort}${path23}`,
27186
+ close: () => new Promise((resolve8, reject) => {
27187
+ server.close((err) => err ? reject(err) : resolve8());
24184
27188
  })
24185
27189
  };
24186
27190
  }
@@ -24852,6 +27856,6 @@ var allServers = () => ({
24852
27856
  ssh: { ...sshManagerServer(), enabled: false }
24853
27857
  });
24854
27858
 
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 };
27859
+ 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
27860
  //# sourceMappingURL=index.js.map
24857
27861
  //# sourceMappingURL=index.js.map