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