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