@gilbertgt/dsh-plan-orchestrator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,3776 @@
1
+ import { A as validatePlanArtifact, C as git, D as normalizeOwnedPath, E as extractPlanArtifact, O as parseValidationCommand, S as fullHead, _ as readJson, b as changedPaths, c as trustedIssueTexts, d as hashBytes, f as assertOwnedPaths, g as confined, h as atomicJson, i as ghRepository, k as schedulerPathIdentity, l as assertTrustedReceipts, m as RunStore, n as ghPreflight, o as prepareIssueBranch, p as disjointOwnership, s as remotePr, t as fetchIssue, u as receiptIndex, v as stateRoot, w as repoRoot, x as decodeUtf8Strict, y as assertRepoPathsConfined } from "./github-Cy4Pm35G.js";
2
+ import { n as validateFixedRoute, r as eligibleTransportFailure, t as installPlannerRoute } from "./planner-route-B9z5GziX.js";
3
+ import { createRequire } from "node:module";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { realpathSync } from "node:fs";
8
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { lstat, mkdir, readFile, readlink, rm, symlink, writeFile } from "node:fs/promises";
10
+ import { setSandboxMode } from "@deepseek-ai/dsh-sandbox-policy";
11
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
12
+ import { fileURLToPath } from "node:url";
13
+ import { z } from "zod";
14
+ //#region node_modules/@deepseek-ai/cosmokit/lib/index.js
15
+ /** Return true when a value is `null` or `undefined`. */
16
+ function isNullable(value) {
17
+ return value === null || value === void 0;
18
+ }
19
+ /** Return true for non-array object values. */
20
+ function isPlainObject(data) {
21
+ return data && typeof data === "object" && !Array.isArray(data);
22
+ }
23
+ /** Filter object entries and return a new object. */
24
+ function filterKeys(object, filter) {
25
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
26
+ }
27
+ /** Map object values while preserving the original key set. */
28
+ function mapValues(object, transform) {
29
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
30
+ }
31
+ /** Pick selected keys from an object, optionally including `undefined` values. */
32
+ function pick(source, keys, forced) {
33
+ if (!keys) return { ...source };
34
+ const result = {};
35
+ for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
36
+ return result;
37
+ }
38
+ /** Test values using `instanceof` with a `toStringTag` fallback. */
39
+ function is(type, value) {
40
+ if (arguments.length === 1) return (value) => is(type, value);
41
+ return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
42
+ }
43
+ function isArrayBufferLike(value) {
44
+ return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
45
+ }
46
+ function isArrayBufferSource(value) {
47
+ return isArrayBufferLike(value) || ArrayBuffer.isView(value);
48
+ }
49
+ /** Binary source detection and base64/hex conversion helpers. */
50
+ var Binary;
51
+ (function(Binary) {
52
+ Binary.is = isArrayBufferLike;
53
+ Binary.isSource = isArrayBufferSource;
54
+ function fromSource(source) {
55
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
56
+ else return source;
57
+ }
58
+ Binary.fromSource = fromSource;
59
+ function toBase64(source) {
60
+ source = fromSource(source);
61
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
62
+ let binary = "";
63
+ const bytes = new Uint8Array(source);
64
+ for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
65
+ return btoa(binary);
66
+ }
67
+ Binary.toBase64 = toBase64;
68
+ function fromBase64(source) {
69
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
70
+ return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
71
+ }
72
+ Binary.fromBase64 = fromBase64;
73
+ function toHex(source) {
74
+ source = fromSource(source);
75
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
76
+ return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
77
+ }
78
+ Binary.toHex = toHex;
79
+ function fromHex(source) {
80
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
81
+ const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
82
+ const buffer = [];
83
+ for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
84
+ return Uint8Array.from(buffer).buffer;
85
+ }
86
+ Binary.fromHex = fromHex;
87
+ })(Binary || (Binary = {}));
88
+ Binary.fromBase64;
89
+ Binary.toBase64;
90
+ Binary.fromHex;
91
+ Binary.toHex;
92
+ /** Deep-clone common JavaScript values while preserving prototypes and cycles. */
93
+ function clone(source, refs = /* @__PURE__ */ new Map()) {
94
+ if (!source || typeof source !== "object") return source;
95
+ if (is("Date", source)) return new Date(source.valueOf());
96
+ if (is("RegExp", source)) return new RegExp(source.source, source.flags);
97
+ if (isArrayBufferLike(source)) return source.slice(0);
98
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
99
+ const cached = refs.get(source);
100
+ if (cached) return cached;
101
+ if (Array.isArray(source)) {
102
+ const result = [];
103
+ refs.set(source, result);
104
+ source.forEach((value, index) => {
105
+ result[index] = Reflect.apply(clone, null, [value, refs]);
106
+ });
107
+ return result;
108
+ }
109
+ const result = Object.create(Object.getPrototypeOf(source));
110
+ refs.set(source, result);
111
+ for (const key of Reflect.ownKeys(source)) {
112
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
113
+ if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
114
+ Reflect.defineProperty(result, key, descriptor);
115
+ }
116
+ return result;
117
+ }
118
+ /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
119
+ function deepEqual(a, b, strict) {
120
+ if (a === b) return true;
121
+ if (!strict && isNullable(a) && isNullable(b)) return true;
122
+ if (typeof a !== typeof b) return false;
123
+ if (typeof a !== "object") return false;
124
+ if (!a || !b) return false;
125
+ function check(test, then) {
126
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
127
+ }
128
+ return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
129
+ if (a.byteLength !== b.byteLength) return false;
130
+ const viewA = new Uint8Array(a);
131
+ const viewB = new Uint8Array(b);
132
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
133
+ return true;
134
+ }) ?? Object.keys({
135
+ ...a,
136
+ ...b
137
+ }).every((key) => deepEqual(a[key], b[key], strict));
138
+ }
139
+ /** Time constants plus parsing and formatting helpers. */
140
+ var Time;
141
+ (function(Time) {
142
+ Time.millisecond = 1;
143
+ Time.second = 1e3;
144
+ Time.minute = Time.second * 60;
145
+ Time.hour = Time.minute * 60;
146
+ Time.day = Time.hour * 24;
147
+ Time.week = Time.day * 7;
148
+ let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
149
+ function setTimezoneOffset(offset) {
150
+ timezoneOffset = offset;
151
+ }
152
+ Time.setTimezoneOffset = setTimezoneOffset;
153
+ function getTimezoneOffset() {
154
+ return timezoneOffset;
155
+ }
156
+ Time.getTimezoneOffset = getTimezoneOffset;
157
+ function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
158
+ if (typeof date === "number") date = new Date(date);
159
+ if (offset === void 0) offset = timezoneOffset;
160
+ return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
161
+ }
162
+ Time.getDateNumber = getDateNumber;
163
+ function fromDateNumber(value, offset) {
164
+ const date = new Date(value * Time.day);
165
+ if (offset === void 0) offset = timezoneOffset;
166
+ return new Date(+date + offset * Time.minute);
167
+ }
168
+ Time.fromDateNumber = fromDateNumber;
169
+ const numeric = /\d+(?:\.\d+)?/.source;
170
+ const timeRegExp = new RegExp(`^${[
171
+ "w(?:eek(?:s)?)?",
172
+ "d(?:ay(?:s)?)?",
173
+ "h(?:our(?:s)?)?",
174
+ "m(?:in(?:ute)?(?:s)?)?",
175
+ "s(?:ec(?:ond)?(?:s)?)?"
176
+ ].map((unit) => `(${numeric}${unit})?`).join("")}$`);
177
+ function parseTime(source) {
178
+ const capture = timeRegExp.exec(source);
179
+ if (!capture) return 0;
180
+ return (parseFloat(capture[1]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
181
+ }
182
+ Time.parseTime = parseTime;
183
+ function parseDate(date) {
184
+ const parsed = parseTime(date);
185
+ if (parsed) date = Date.now() + parsed;
186
+ else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
187
+ else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
188
+ return date ? new Date(date) : /* @__PURE__ */ new Date();
189
+ }
190
+ Time.parseDate = parseDate;
191
+ function format(ms) {
192
+ const abs = Math.abs(ms);
193
+ if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
194
+ else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
195
+ else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
196
+ else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
197
+ return ms + "ms";
198
+ }
199
+ Time.format = format;
200
+ function toDigits(source, length = 2) {
201
+ return source.toString().padStart(length, "0");
202
+ }
203
+ Time.toDigits = toDigits;
204
+ function template(template, time = /* @__PURE__ */ new Date()) {
205
+ return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
206
+ }
207
+ Time.template = template;
208
+ })(Time || (Time = {}));
209
+ //#endregion
210
+ //#region node_modules/@deepseek-ai/schemastery/lib/index.mjs
211
+ const kSchema = Symbol.for("schemastery");
212
+ const kValidationError = Symbol.for("ValidationError");
213
+ globalThis.__schemastery_index__ ??= 0;
214
+ globalThis.__schemastery_refs__ = void 0;
215
+ var ValidationError = class extends TypeError {
216
+ options;
217
+ name = "ValidationError";
218
+ constructor(message, options) {
219
+ let prefix = "$";
220
+ for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
221
+ else if (typeof segment === "number") prefix += "[" + segment + "]";
222
+ else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
223
+ if (prefix.startsWith(".")) prefix = prefix.slice(1);
224
+ super((prefix === "$" ? "" : `${prefix} `) + message);
225
+ this.options = options;
226
+ }
227
+ static is(error) {
228
+ return !!error?.[kValidationError];
229
+ }
230
+ };
231
+ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
232
+ const Schema = function(options) {
233
+ const schema = function(data, options = {}) {
234
+ return Schema.resolve(data, schema, options)[0];
235
+ };
236
+ if (options.refs) {
237
+ const refs = mapValues(options.refs, (options) => new Schema(options));
238
+ const getRef = (uid) => refs[uid];
239
+ for (const key in refs) {
240
+ const options = refs[key];
241
+ options.sKey = getRef(options.sKey);
242
+ options.inner = getRef(options.inner);
243
+ options.list = options.list && options.list.map(getRef);
244
+ options.dict = options.dict && mapValues(options.dict, getRef);
245
+ }
246
+ return refs[options.uid];
247
+ }
248
+ Object.assign(schema, options);
249
+ if (typeof schema.callback === "string") try {
250
+ schema.callback = new Function("return " + schema.callback)();
251
+ } catch {}
252
+ Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
253
+ Object.setPrototypeOf(schema, Schema.prototype);
254
+ schema.meta ||= {};
255
+ schema.toString = schema.toString.bind(schema);
256
+ return schema;
257
+ };
258
+ Schema.prototype = Object.create(Function.prototype);
259
+ Schema.prototype[kSchema] = true;
260
+ Object.defineProperty(Schema.prototype, "~standard", { get() {
261
+ return {
262
+ version: 1,
263
+ vendor: "schemastery",
264
+ validate: (value) => {
265
+ try {
266
+ return { value: Schema.resolve(value, this, {})[0] };
267
+ } catch (error) {
268
+ if (ValidationError.is(error)) return { issues: [{
269
+ message: error.message,
270
+ path: error.options.path
271
+ }] };
272
+ throw error;
273
+ }
274
+ }
275
+ };
276
+ } });
277
+ Schema.ValidationError = ValidationError;
278
+ Schema.prototype.toJSON = function toJSON() {
279
+ if (globalThis.__schemastery_refs__) {
280
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
281
+ return this.uid;
282
+ }
283
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
284
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
285
+ const result = {
286
+ uid: this.uid,
287
+ refs: globalThis.__schemastery_refs__
288
+ };
289
+ globalThis.__schemastery_refs__ = void 0;
290
+ return result;
291
+ };
292
+ Schema.prototype.set = function set(key, value) {
293
+ this.dict[key] = value;
294
+ return this;
295
+ };
296
+ Schema.prototype.push = function push(value) {
297
+ this.list.push(value);
298
+ return this;
299
+ };
300
+ function mergeDesc(original, messages) {
301
+ const result = typeof original === "string" ? { "": original } : { ...original };
302
+ for (const locale in messages) {
303
+ const value = messages[locale];
304
+ if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
305
+ else if (typeof value === "string") result[locale] = value;
306
+ }
307
+ return result;
308
+ }
309
+ function getInner(value) {
310
+ return value?.$value ?? value?.$inner;
311
+ }
312
+ function extractKeys(data) {
313
+ return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
314
+ }
315
+ Schema.prototype.i18n = function i18n(messages) {
316
+ const schema = Schema(this);
317
+ const desc = mergeDesc(schema.meta.description, messages);
318
+ if (Object.keys(desc).length) schema.meta.description = desc;
319
+ if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
320
+ return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
321
+ });
322
+ if (schema.list) schema.list = schema.list.map((inner, index) => {
323
+ return inner.i18n(mapValues(messages, (data = {}) => {
324
+ if (Array.isArray(getInner(data))) return getInner(data)[index];
325
+ if (Array.isArray(data)) return data[index];
326
+ return extractKeys(data);
327
+ }));
328
+ });
329
+ if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
330
+ if (getInner(data)) return getInner(data);
331
+ return extractKeys(data);
332
+ }));
333
+ if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
334
+ return schema;
335
+ };
336
+ Schema.prototype.extra = function extra(key, value) {
337
+ const schema = Schema(this);
338
+ schema.meta = {
339
+ ...schema.meta,
340
+ [key]: value
341
+ };
342
+ return schema;
343
+ };
344
+ for (const key of [
345
+ "required",
346
+ "disabled",
347
+ "collapse",
348
+ "hidden",
349
+ "loose"
350
+ ]) Object.assign(Schema.prototype, { [key](value = true) {
351
+ const schema = Schema(this);
352
+ schema.meta = {
353
+ ...schema.meta,
354
+ [key]: value
355
+ };
356
+ return schema;
357
+ } });
358
+ Schema.prototype.deprecated = function deprecated() {
359
+ const schema = Schema(this);
360
+ schema.meta.badges ||= [];
361
+ schema.meta.badges.push({
362
+ text: "deprecated",
363
+ type: "danger"
364
+ });
365
+ return schema;
366
+ };
367
+ Schema.prototype.experimental = function experimental() {
368
+ const schema = Schema(this);
369
+ schema.meta.badges ||= [];
370
+ schema.meta.badges.push({
371
+ text: "experimental",
372
+ type: "warning"
373
+ });
374
+ return schema;
375
+ };
376
+ Schema.prototype.pattern = function pattern(regexp) {
377
+ const schema = Schema(this);
378
+ const pattern = pick(regexp, ["source", "flags"]);
379
+ schema.meta = {
380
+ ...schema.meta,
381
+ pattern
382
+ };
383
+ return schema;
384
+ };
385
+ Schema.prototype.simplify = function simplify(value) {
386
+ if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
387
+ if (isNullable(value)) return value;
388
+ if (this.type === "object" || this.type === "dict") {
389
+ const result = {};
390
+ for (const key in value) {
391
+ const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
392
+ if (this.type === "dict" || !isNullable(item)) result[key] = item;
393
+ }
394
+ if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
395
+ return result;
396
+ } else if (this.type === "array" || this.type === "tuple") {
397
+ const result = [];
398
+ value.forEach((value, index) => {
399
+ const schema = this.type === "array" ? this.inner : this.list[index];
400
+ const item = schema ? schema.simplify(value) : value;
401
+ result.push(item);
402
+ });
403
+ return result;
404
+ } else if (this.type === "intersect") {
405
+ const result = {};
406
+ for (const item of this.list) Object.assign(result, item.simplify(value));
407
+ return result;
408
+ } else if (this.type === "union") for (const schema of this.list) try {
409
+ Schema.resolve(value, schema, {});
410
+ return schema.simplify(value);
411
+ } catch {}
412
+ return value;
413
+ };
414
+ Schema.prototype.toString = function toString(inline) {
415
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
416
+ };
417
+ Schema.prototype.role = function role(role, extra) {
418
+ const schema = Schema(this);
419
+ schema.meta = {
420
+ ...schema.meta,
421
+ role,
422
+ extra
423
+ };
424
+ return schema;
425
+ };
426
+ for (const key of [
427
+ "default",
428
+ "link",
429
+ "comment",
430
+ "description",
431
+ "max",
432
+ "min",
433
+ "step"
434
+ ]) Object.assign(Schema.prototype, { [key](value) {
435
+ const schema = Schema(this);
436
+ schema.meta = {
437
+ ...schema.meta,
438
+ [key]: value
439
+ };
440
+ return schema;
441
+ } });
442
+ const resolvers = {};
443
+ Schema.extend = function extend(type, resolve) {
444
+ resolvers[type] = resolve;
445
+ };
446
+ Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
447
+ if (!schema) return [data];
448
+ if (options.ignore?.(data, schema)) return [data];
449
+ if (isNullable(data) && schema.type !== "lazy") {
450
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options);
451
+ let current = schema;
452
+ let fallback = schema.meta.default;
453
+ while (current?.type === "intersect" && isNullable(fallback)) {
454
+ current = current.list[0];
455
+ fallback = current?.meta.default;
456
+ }
457
+ if (isNullable(fallback)) return [data];
458
+ data = clone(fallback);
459
+ }
460
+ const callback = resolvers[schema.type];
461
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
462
+ try {
463
+ return callback(data, schema, options, strict);
464
+ } catch (error) {
465
+ if (!schema.meta.loose) throw error;
466
+ return [schema.meta.default];
467
+ }
468
+ };
469
+ Schema.from = function from(source) {
470
+ if (isNullable(source)) return Schema.any();
471
+ else if ([
472
+ "string",
473
+ "number",
474
+ "boolean"
475
+ ].includes(typeof source)) return Schema.const(source).required();
476
+ else if (source[kSchema]) return source;
477
+ else if (typeof source === "function") switch (source) {
478
+ case String: return Schema.string().required();
479
+ case Number: return Schema.number().required();
480
+ case Boolean: return Schema.boolean().required();
481
+ case Function: return Schema.function().required();
482
+ default: return Schema.is(source).required();
483
+ }
484
+ else throw new TypeError(`cannot infer schema from ${source}`);
485
+ };
486
+ Schema.lazy = function lazy(builder) {
487
+ const toJSON = () => {
488
+ if (!schema.inner[kSchema]) {
489
+ schema.inner = schema.builder();
490
+ schema.inner.meta = {
491
+ ...schema.meta,
492
+ ...schema.inner.meta
493
+ };
494
+ }
495
+ return schema.inner.toJSON();
496
+ };
497
+ const schema = new Schema({
498
+ type: "lazy",
499
+ builder,
500
+ inner: { toJSON }
501
+ });
502
+ return schema;
503
+ };
504
+ Schema.natural = function natural() {
505
+ return Schema.number().step(1).min(0);
506
+ };
507
+ Schema.percent = function percent() {
508
+ return Schema.number().step(.01).min(0).max(1).role("slider");
509
+ };
510
+ Schema.date = function date() {
511
+ return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
512
+ const date = new Date(value);
513
+ if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
514
+ return date;
515
+ }, true)]);
516
+ };
517
+ Schema.regExp = function regExp(flag = "") {
518
+ return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
519
+ try {
520
+ return new RegExp(value, flag);
521
+ } catch (e) {
522
+ throw new ValidationError(e.message, options);
523
+ }
524
+ }, true)]);
525
+ };
526
+ Schema.arrayBuffer = function arrayBuffer(encoding) {
527
+ return Schema.union([
528
+ Schema.is(ArrayBuffer),
529
+ Schema.is(SharedArrayBuffer),
530
+ Schema.transform(Schema.any(), (value, options) => {
531
+ if (Binary.isSource(value)) return Binary.fromSource(value);
532
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
533
+ }, true),
534
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
535
+ try {
536
+ return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
537
+ } catch (e) {
538
+ throw new ValidationError(e.message, options);
539
+ }
540
+ }, true)] : []
541
+ ]);
542
+ };
543
+ Schema.extend("lazy", (data, schema, options, strict) => {
544
+ if (!schema.inner[kSchema]) {
545
+ schema.inner = schema.builder();
546
+ schema.inner.meta = {
547
+ ...schema.meta,
548
+ ...schema.inner.meta
549
+ };
550
+ }
551
+ return Schema.resolve(data, schema.inner, options, strict);
552
+ });
553
+ Schema.extend("any", (data) => {
554
+ return [data];
555
+ });
556
+ Schema.extend("never", (data, _, options) => {
557
+ throw new ValidationError(`expected nullable but got ${data}`, options);
558
+ });
559
+ Schema.extend("const", (data, { value }, options) => {
560
+ if (deepEqual(data, value)) return [value];
561
+ throw new ValidationError(`expected ${value} but got ${data}`, options);
562
+ });
563
+ function checkWithinRange(data, meta, description, options, skipMin = false) {
564
+ const { max = Infinity, min = -Infinity } = meta;
565
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
566
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
567
+ }
568
+ Schema.extend("string", (data, { meta }, options) => {
569
+ if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
570
+ if (meta.pattern) {
571
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
572
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
573
+ }
574
+ checkWithinRange(data.length, meta, "string length", options);
575
+ return [data];
576
+ });
577
+ function decimalShift(data, digits) {
578
+ const str = data.toString();
579
+ if (str.includes("e")) return data * Math.pow(10, digits);
580
+ const index = str.indexOf(".");
581
+ if (index === -1) return data * Math.pow(10, digits);
582
+ const frac = str.slice(index + 1);
583
+ const integer = str.slice(0, index);
584
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
585
+ return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
586
+ }
587
+ function isMultipleOf(data, min, step) {
588
+ step = Math.abs(step);
589
+ if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
590
+ const index = step.toString().indexOf(".");
591
+ const digits = step.toString().slice(index + 1).length;
592
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
593
+ }
594
+ Schema.extend("number", (data, { meta }, options) => {
595
+ if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
596
+ checkWithinRange(data, meta, "number", options);
597
+ const { step } = meta;
598
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
599
+ return [data];
600
+ });
601
+ Schema.extend("boolean", (data, _, options) => {
602
+ if (typeof data === "boolean") return [data];
603
+ throw new ValidationError(`expected boolean but got ${data}`, options);
604
+ });
605
+ Schema.extend("bitset", (data, { bits, meta }, options) => {
606
+ let value = 0, keys = [];
607
+ if (typeof data === "number") {
608
+ value = data;
609
+ for (const key in bits) if (data & bits[key]) keys.push(key);
610
+ } else if (Array.isArray(data)) {
611
+ keys = data;
612
+ for (const key of keys) {
613
+ if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
614
+ if (key in bits) value |= bits[key];
615
+ }
616
+ } else throw new ValidationError(`expected number or array but got ${data}`, options);
617
+ if (value === meta.default) return [value];
618
+ return [value, keys];
619
+ });
620
+ Schema.extend("function", (data, _, options) => {
621
+ if (typeof data === "function") return [data];
622
+ throw new ValidationError(`expected function but got ${data}`, options);
623
+ });
624
+ Schema.extend("is", (data, { constructor }, options) => {
625
+ if (typeof constructor === "function") {
626
+ if (data instanceof constructor) return [data];
627
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
628
+ } else {
629
+ if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
630
+ let prototype = Object.getPrototypeOf(data);
631
+ while (prototype) {
632
+ if (prototype.constructor?.name === constructor) return [data];
633
+ prototype = Object.getPrototypeOf(prototype);
634
+ }
635
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options);
636
+ }
637
+ });
638
+ function property(data, key, schema, options) {
639
+ try {
640
+ const [value, adapted] = Schema.resolve(data[key], schema, {
641
+ ...options,
642
+ path: [...options.path || [], key]
643
+ });
644
+ if (adapted !== void 0) data[key] = adapted;
645
+ return value;
646
+ } catch (e) {
647
+ if (!options?.autofix) throw e;
648
+ delete data[key];
649
+ return schema.meta.default;
650
+ }
651
+ }
652
+ Schema.extend("array", (data, { inner, meta }, options) => {
653
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
654
+ checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
655
+ return [data.map((_, index) => property(data, index, inner, options))];
656
+ });
657
+ Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
658
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
659
+ const result = {};
660
+ for (const key in data) {
661
+ let rKey;
662
+ try {
663
+ rKey = Schema.resolve(key, sKey, options)[0];
664
+ } catch (error) {
665
+ if (strict) continue;
666
+ throw error;
667
+ }
668
+ result[rKey] = property(data, key, inner, options);
669
+ data[rKey] = data[key];
670
+ if (key !== rKey) delete data[key];
671
+ }
672
+ return [result];
673
+ });
674
+ Schema.extend("tuple", (data, { list }, options, strict) => {
675
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
676
+ const result = list.map((inner, index) => property(data, index, inner, options));
677
+ if (strict) return [result];
678
+ result.push(...data.slice(list.length));
679
+ return [result];
680
+ });
681
+ function merge(result, data) {
682
+ for (const key in data) {
683
+ if (key in result) continue;
684
+ result[key] = data[key];
685
+ }
686
+ }
687
+ Schema.extend("object", (data, { dict }, options, strict) => {
688
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
689
+ const result = {};
690
+ for (const key in dict) {
691
+ const value = property(data, key, dict[key], options);
692
+ if (!isNullable(value) || key in data) result[key] = value;
693
+ }
694
+ if (!strict) merge(result, data);
695
+ return [result];
696
+ });
697
+ Schema.extend("union", (data, { list, toString }, options, strict) => {
698
+ const messages = [];
699
+ for (const inner of list) try {
700
+ return Schema.resolve(data, inner, options, strict);
701
+ } catch (error) {
702
+ messages.push(error);
703
+ }
704
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
705
+ });
706
+ Schema.extend("intersect", (data, { list, toString }, options, strict) => {
707
+ if (!list.length) return [data];
708
+ let result;
709
+ for (const inner of list) {
710
+ const value = Schema.resolve(data, inner, options, true)[0];
711
+ if (isNullable(value)) continue;
712
+ if (isNullable(result)) result = value;
713
+ else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
714
+ else if (typeof value === "object") merge(result ??= {}, value);
715
+ else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
716
+ }
717
+ if (!strict && isPlainObject(data)) merge(result, data);
718
+ return [result];
719
+ });
720
+ Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
721
+ const [result, adapted = data] = Schema.resolve(data, inner, options, true);
722
+ if (preserve) return [callback(result)];
723
+ else return [callback(result), callback(adapted)];
724
+ });
725
+ const formatters = {};
726
+ function defineMethod(name, keys, format) {
727
+ formatters[name] = format;
728
+ Object.assign(Schema, { [name](...args) {
729
+ const schema = new Schema({ type: name });
730
+ keys.forEach((key, index) => {
731
+ switch (key) {
732
+ case "sKey":
733
+ schema.sKey = args[index] ?? Schema.string();
734
+ break;
735
+ case "inner":
736
+ schema.inner = Schema.from(args[index]);
737
+ break;
738
+ case "list":
739
+ schema.list = args[index].map(Schema.from);
740
+ break;
741
+ case "dict":
742
+ schema.dict = mapValues(args[index], Schema.from);
743
+ break;
744
+ case "bits":
745
+ schema.bits = {};
746
+ for (const key in args[index]) {
747
+ if (typeof args[index][key] !== "number") continue;
748
+ schema.bits[key] = args[index][key];
749
+ }
750
+ break;
751
+ case "callback": {
752
+ const callback = schema.callback = args[index];
753
+ callback["toJSON"] ||= () => callback.toString();
754
+ break;
755
+ }
756
+ case "constructor": {
757
+ const constructor = schema.constructor = args[index];
758
+ if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
759
+ break;
760
+ }
761
+ default: schema[key] = args[index];
762
+ }
763
+ });
764
+ if (name === "object" || name === "dict") schema.meta.default = {};
765
+ else if (name === "array" || name === "tuple") schema.meta.default = [];
766
+ else if (name === "bitset") schema.meta.default = 0;
767
+ return schema;
768
+ } });
769
+ }
770
+ defineMethod("is", ["constructor"], ({ constructor }) => {
771
+ if (typeof constructor === "function") return constructor.name;
772
+ else return constructor;
773
+ });
774
+ defineMethod("any", [], () => "any");
775
+ defineMethod("never", [], () => "never");
776
+ defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
777
+ defineMethod("string", [], () => "string");
778
+ defineMethod("number", [], () => "number");
779
+ defineMethod("boolean", [], () => "boolean");
780
+ defineMethod("bitset", ["bits"], () => "bitset");
781
+ defineMethod("function", [], () => "function");
782
+ defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
783
+ defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
784
+ defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
785
+ defineMethod("object", ["dict"], ({ dict }) => {
786
+ if (Object.keys(dict).length === 0) return "{}";
787
+ return `{ ${Object.entries(dict).map(([key, inner]) => {
788
+ return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
789
+ }).join(", ")} }`;
790
+ });
791
+ defineMethod("union", ["list"], ({ list }, inline) => {
792
+ const result = list.map(({ toString: format }) => format()).join(" | ");
793
+ return inline ? `(${result})` : result;
794
+ });
795
+ defineMethod("intersect", ["list"], ({ list }) => {
796
+ return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
797
+ });
798
+ defineMethod("transform", [
799
+ "inner",
800
+ "callback",
801
+ "preserve"
802
+ ], ({ inner }, isInner) => inner.toString(isInner));
803
+ //#endregion
804
+ //#region src/contract/settings.ts
805
+ const SETTINGS_NAMESPACE = "plan-orchestrator";
806
+ const ROLE_NAMES = [
807
+ "planner",
808
+ "worker",
809
+ "integrator",
810
+ "reviewer"
811
+ ];
812
+ const SETTINGS_KEYS = /* @__PURE__ */ new Set([
813
+ "enabled",
814
+ "roles",
815
+ "planning",
816
+ "execution",
817
+ "review",
818
+ "recovery",
819
+ "externalIssue",
820
+ "workspaceOverrides"
821
+ ]);
822
+ const ROUTE_KEYS = /* @__PURE__ */ new Set([
823
+ "mode",
824
+ "provider",
825
+ "model",
826
+ "reasoningEffort",
827
+ "maxTokens",
828
+ "fallbacks"
829
+ ]);
830
+ const CHOICE_KEYS = /* @__PURE__ */ new Set([
831
+ "provider",
832
+ "model",
833
+ "reasoningEffort",
834
+ "maxTokens"
835
+ ]);
836
+ const PLANNING_KEYS = /* @__PURE__ */ new Set([
837
+ "adaptiveResearch",
838
+ "strictReadOnly",
839
+ "maxInitialReadFiles",
840
+ "softInputTokens",
841
+ "progressiveDiscovery",
842
+ "requireExpansionReason"
843
+ ]);
844
+ const EXECUTION_KEYS = /* @__PURE__ */ new Set([
845
+ "maxParallelWorkers",
846
+ "parallelMode",
847
+ "requireExplicitOwnership",
848
+ "keepFailedWorktrees",
849
+ "sdkProfile",
850
+ "roleTimeoutMs"
851
+ ]);
852
+ const REVIEW_KEYS = /* @__PURE__ */ new Set([
853
+ "maxReviewRounds",
854
+ "protocolRetry",
855
+ "trustedValidation",
856
+ "outputCapBytes"
857
+ ]);
858
+ const RECOVERY_KEYS = /* @__PURE__ */ new Set(["allowSafeResume"]);
859
+ const EXTERNAL_KEYS = /* @__PURE__ */ new Set(["enabled", "publishAfterPass"]);
860
+ const WORKSPACE_KEYS = /* @__PURE__ */ new Set(["roles"]);
861
+ const defaultRoute = () => ({
862
+ mode: "current",
863
+ fallbacks: []
864
+ });
865
+ const DEFAULT_SETTINGS = Object.freeze({
866
+ enabled: true,
867
+ roles: {
868
+ planner: defaultRoute(),
869
+ worker: defaultRoute(),
870
+ integrator: defaultRoute(),
871
+ reviewer: defaultRoute()
872
+ },
873
+ planning: {
874
+ adaptiveResearch: true,
875
+ strictReadOnly: true,
876
+ maxInitialReadFiles: 6,
877
+ softInputTokens: 2e4,
878
+ progressiveDiscovery: true,
879
+ requireExpansionReason: true
880
+ },
881
+ execution: {
882
+ maxParallelWorkers: 3,
883
+ parallelMode: "auto",
884
+ requireExplicitOwnership: true,
885
+ keepFailedWorktrees: true,
886
+ sdkProfile: "sdk",
887
+ roleTimeoutMs: 9e5
888
+ },
889
+ review: {
890
+ maxReviewRounds: 2,
891
+ protocolRetry: 1,
892
+ trustedValidation: true,
893
+ outputCapBytes: 16 * 1024 * 1024
894
+ },
895
+ recovery: { allowSafeResume: true },
896
+ externalIssue: {
897
+ enabled: false,
898
+ publishAfterPass: false
899
+ },
900
+ workspaceOverrides: {}
901
+ });
902
+ function object(value, where) {
903
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${where} must be an object`);
904
+ return value;
905
+ }
906
+ function strictKeys(value, allowed, where) {
907
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
908
+ if (unknown.length > 0) throw new Error(`${where} has unknown field(s): ${unknown.join(", ")}`);
909
+ }
910
+ function bool(value, where) {
911
+ if (typeof value !== "boolean") throw new Error(`${where} must be boolean`);
912
+ return value;
913
+ }
914
+ function integer(value, where, min, max) {
915
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${where} must be an integer from ${min} through ${max}`);
916
+ return value;
917
+ }
918
+ function optionalString(value, where, max = 256) {
919
+ if (value === void 0) return void 0;
920
+ if (typeof value !== "string" || value.length === 0 || value.length > max || value.includes("\0")) throw new Error(`${where} invalid`);
921
+ return value;
922
+ }
923
+ function routeChoice(value, where) {
924
+ const x = object(value, where);
925
+ strictKeys(x, CHOICE_KEYS, where);
926
+ const provider = optionalString(x.provider, `${where}.provider`);
927
+ const model = optionalString(x.model, `${where}.model`);
928
+ if (!provider || !model) throw new Error(`${where} needs provider and model`);
929
+ const reasoningEffort = optionalString(x.reasoningEffort, `${where}.reasoningEffort`, 64);
930
+ const maxTokens = x.maxTokens === void 0 ? void 0 : integer(x.maxTokens, `${where}.maxTokens`, 1, 2e6);
931
+ return {
932
+ provider,
933
+ model,
934
+ ...reasoningEffort ? { reasoningEffort } : {},
935
+ ...maxTokens ? { maxTokens } : {}
936
+ };
937
+ }
938
+ function roleRoute(value, where, workspace) {
939
+ const x = object(value, where);
940
+ strictKeys(x, ROUTE_KEYS, where);
941
+ const allowedModes = workspace ? [
942
+ "inherit",
943
+ "current",
944
+ "fixed"
945
+ ] : ["current", "fixed"];
946
+ if (typeof x.mode !== "string" || !allowedModes.includes(x.mode)) throw new Error(`${where}.mode invalid`);
947
+ const mode = x.mode;
948
+ const provider = optionalString(x.provider, `${where}.provider`);
949
+ const model = optionalString(x.model, `${where}.model`);
950
+ const reasoningEffort = optionalString(x.reasoningEffort, `${where}.reasoningEffort`, 64);
951
+ const maxTokens = x.maxTokens === void 0 ? void 0 : integer(x.maxTokens, `${where}.maxTokens`, 1, 2e6);
952
+ if (mode === "fixed" && (!provider || !model)) throw new Error(`${where} fixed route needs provider and model`);
953
+ if (mode !== "fixed" && (provider !== void 0 || model !== void 0 || reasoningEffort !== void 0 || maxTokens !== void 0)) throw new Error(`${where} ${mode} route cannot carry fixed-route fields`);
954
+ if (!Array.isArray(x.fallbacks) || x.fallbacks.length > 8) throw new Error(`${where}.fallbacks invalid`);
955
+ return {
956
+ mode,
957
+ fallbacks: x.fallbacks.map((item, index) => routeChoice(item, `${where}.fallbacks[${index}]`)),
958
+ ...provider ? { provider } : {},
959
+ ...model ? { model } : {},
960
+ ...reasoningEffort ? { reasoningEffort } : {},
961
+ ...maxTokens ? { maxTokens } : {}
962
+ };
963
+ }
964
+ function section(value, allowed, where) {
965
+ const x = object(value, where);
966
+ strictKeys(x, allowed, where);
967
+ return x;
968
+ }
969
+ function validateSettings(value) {
970
+ const x = object(value, "settings");
971
+ strictKeys(x, SETTINGS_KEYS, "settings");
972
+ const rolesRaw = object(x.roles, "settings.roles");
973
+ strictKeys(rolesRaw, new Set(ROLE_NAMES), "settings.roles");
974
+ const roles = Object.fromEntries(ROLE_NAMES.map((role) => [role, roleRoute(rolesRaw[role], `settings.roles.${role}`, false)]));
975
+ const planning = section(x.planning, PLANNING_KEYS, "settings.planning");
976
+ const execution = section(x.execution, EXECUTION_KEYS, "settings.execution");
977
+ const review = section(x.review, REVIEW_KEYS, "settings.review");
978
+ const recovery = section(x.recovery, RECOVERY_KEYS, "settings.recovery");
979
+ const externalIssue = section(x.externalIssue, EXTERNAL_KEYS, "settings.externalIssue");
980
+ const workspaceRaw = object(x.workspaceOverrides, "settings.workspaceOverrides");
981
+ const workspaceOverrides = {};
982
+ if (Object.keys(workspaceRaw).length > 256) throw new Error("settings.workspaceOverrides has too many entries");
983
+ for (const [workspaceKey, raw] of Object.entries(workspaceRaw)) {
984
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(workspaceKey)) throw new Error(`invalid workspace override key: ${workspaceKey}`);
985
+ const override = object(raw, `settings.workspaceOverrides.${workspaceKey}`);
986
+ strictKeys(override, WORKSPACE_KEYS, `settings.workspaceOverrides.${workspaceKey}`);
987
+ const result = {};
988
+ if (override.roles !== void 0) {
989
+ const roleObject = object(override.roles, `settings.workspaceOverrides.${workspaceKey}.roles`);
990
+ strictKeys(roleObject, new Set(ROLE_NAMES), `settings.workspaceOverrides.${workspaceKey}.roles`);
991
+ result.roles = {};
992
+ for (const [role, route] of Object.entries(roleObject)) result.roles[role] = roleRoute(route, `settings.workspaceOverrides.${workspaceKey}.roles.${role}`, true);
993
+ }
994
+ workspaceOverrides[workspaceKey] = result;
995
+ }
996
+ if (bool(execution.requireExplicitOwnership, "settings.execution.requireExplicitOwnership") !== true) throw new Error("settings.execution.requireExplicitOwnership is a security invariant and must be true");
997
+ if (bool(review.trustedValidation, "settings.review.trustedValidation") !== true) throw new Error("settings.review.trustedValidation is a security invariant and must be true");
998
+ const parallelMode = execution.parallelMode;
999
+ if (![
1000
+ "auto",
1001
+ "serial",
1002
+ "worktree"
1003
+ ].includes(String(parallelMode))) throw new Error("settings.execution.parallelMode invalid");
1004
+ return {
1005
+ enabled: bool(x.enabled, "settings.enabled"),
1006
+ roles,
1007
+ planning: {
1008
+ adaptiveResearch: bool(planning.adaptiveResearch, "settings.planning.adaptiveResearch"),
1009
+ strictReadOnly: bool(planning.strictReadOnly, "settings.planning.strictReadOnly"),
1010
+ maxInitialReadFiles: integer(planning.maxInitialReadFiles, "settings.planning.maxInitialReadFiles", 1, 32),
1011
+ softInputTokens: integer(planning.softInputTokens, "settings.planning.softInputTokens", 1e3, 2e6),
1012
+ progressiveDiscovery: bool(planning.progressiveDiscovery, "settings.planning.progressiveDiscovery"),
1013
+ requireExpansionReason: bool(planning.requireExpansionReason, "settings.planning.requireExpansionReason")
1014
+ },
1015
+ execution: {
1016
+ maxParallelWorkers: integer(execution.maxParallelWorkers, "settings.execution.maxParallelWorkers", 1, 8),
1017
+ parallelMode,
1018
+ requireExplicitOwnership: true,
1019
+ keepFailedWorktrees: bool(execution.keepFailedWorktrees, "settings.execution.keepFailedWorktrees"),
1020
+ sdkProfile: optionalString(execution.sdkProfile, "settings.execution.sdkProfile", 128),
1021
+ roleTimeoutMs: integer(execution.roleTimeoutMs, "settings.execution.roleTimeoutMs", 1e3, 36e5)
1022
+ },
1023
+ review: {
1024
+ maxReviewRounds: integer(review.maxReviewRounds, "settings.review.maxReviewRounds", 0, 5),
1025
+ protocolRetry: integer(review.protocolRetry, "settings.review.protocolRetry", 0, 1),
1026
+ trustedValidation: true,
1027
+ outputCapBytes: integer(review.outputCapBytes, "settings.review.outputCapBytes", 1024, 16 * 1024 * 1024)
1028
+ },
1029
+ recovery: { allowSafeResume: bool(recovery.allowSafeResume, "settings.recovery.allowSafeResume") },
1030
+ externalIssue: {
1031
+ enabled: bool(externalIssue.enabled, "settings.externalIssue.enabled"),
1032
+ publishAfterPass: bool(externalIssue.publishAfterPass, "settings.externalIssue.publishAfterPass")
1033
+ },
1034
+ workspaceOverrides
1035
+ };
1036
+ }
1037
+ function mergeSettings(base, patch) {
1038
+ const roles = {
1039
+ ...base.roles,
1040
+ ...patch.roles ?? {}
1041
+ };
1042
+ return validateSettings({
1043
+ ...base,
1044
+ ...patch,
1045
+ roles,
1046
+ planning: {
1047
+ ...base.planning,
1048
+ ...patch.planning ?? {}
1049
+ },
1050
+ execution: {
1051
+ ...base.execution,
1052
+ ...patch.execution ?? {}
1053
+ },
1054
+ review: {
1055
+ ...base.review,
1056
+ ...patch.review ?? {}
1057
+ },
1058
+ recovery: {
1059
+ ...base.recovery,
1060
+ ...patch.recovery ?? {}
1061
+ },
1062
+ externalIssue: {
1063
+ ...base.externalIssue,
1064
+ ...patch.externalIssue ?? {}
1065
+ },
1066
+ workspaceOverrides: {
1067
+ ...base.workspaceOverrides,
1068
+ ...patch.workspaceOverrides ?? {}
1069
+ }
1070
+ });
1071
+ }
1072
+ function effectiveRoleRoute(settings, role, workspaceKey) {
1073
+ const global = settings.roles[role];
1074
+ if (!workspaceKey) return structuredClone(global);
1075
+ const override = settings.workspaceOverrides[workspaceKey]?.roles?.[role];
1076
+ if (!override || override.mode === "inherit") return structuredClone(global);
1077
+ return structuredClone({
1078
+ ...override,
1079
+ mode: override.mode
1080
+ });
1081
+ }
1082
+ //#endregion
1083
+ //#region src/settings-service.ts
1084
+ const settingsEnvelope = Schema.any().default(DEFAULT_SETTINGS);
1085
+ function canonicalWorkspaceIdentity(cwd) {
1086
+ let canonical = resolve(cwd);
1087
+ try {
1088
+ canonical = realpathSync.native(canonical);
1089
+ } catch {}
1090
+ if (process.platform === "win32") canonical = canonical.toLocaleLowerCase("en-US");
1091
+ return canonical;
1092
+ }
1093
+ function workspaceKey(cwd) {
1094
+ return createHash("sha256").update(canonicalWorkspaceIdentity(cwd)).digest("base64url");
1095
+ }
1096
+ function registerSettings(ctx) {
1097
+ const handle = ctx.settings.register(SETTINGS_NAMESPACE, settingsEnvelope, {
1098
+ applies: "live",
1099
+ validate: (value) => {
1100
+ validateSettings(value);
1101
+ }
1102
+ });
1103
+ const get = () => validateSettings(handle.get() ?? DEFAULT_SETTINGS);
1104
+ const update = async (patch) => {
1105
+ const next = mergeSettings(get(), patch);
1106
+ await handle.update(next);
1107
+ };
1108
+ const effective = (cwd) => {
1109
+ const value = get();
1110
+ if (!cwd) return value;
1111
+ const key = workspaceKey(cwd);
1112
+ return {
1113
+ ...value,
1114
+ roles: Object.fromEntries([
1115
+ "planner",
1116
+ "worker",
1117
+ "integrator",
1118
+ "reviewer"
1119
+ ].map((role) => [role, effectiveRoleRoute(value, role, key)]))
1120
+ };
1121
+ };
1122
+ return {
1123
+ get,
1124
+ effective,
1125
+ update,
1126
+ watch: (fn) => handle.watch(() => fn(get())),
1127
+ writable: () => Boolean(ctx.settings.writable)
1128
+ };
1129
+ }
1130
+ //#endregion
1131
+ //#region src/planning/native-plan-bridge.ts
1132
+ var NativePlanBridge = class {
1133
+ #staged = /* @__PURE__ */ new Map();
1134
+ key(sessionId, toolCallId) {
1135
+ return `${sessionId}\0${toolCallId}`;
1136
+ }
1137
+ stage(sessionId, toolCallId, plan, baselineHead, requireOwnership = true) {
1138
+ const { artifact, hash } = extractPlanArtifact(plan, requireOwnership);
1139
+ const staged = {
1140
+ sessionId,
1141
+ toolCallId,
1142
+ plan,
1143
+ artifact,
1144
+ hash,
1145
+ baselineHead,
1146
+ stagedAt: (/* @__PURE__ */ new Date()).toISOString()
1147
+ };
1148
+ this.#staged.set(this.key(sessionId, toolCallId), staged);
1149
+ return staged;
1150
+ }
1151
+ get(sessionId, toolCallId) {
1152
+ return this.#staged.get(this.key(sessionId, toolCallId));
1153
+ }
1154
+ consume(sessionId, toolCallId) {
1155
+ const key = this.key(sessionId, toolCallId), value = this.#staged.get(key);
1156
+ if (value) this.#staged.delete(key);
1157
+ return value;
1158
+ }
1159
+ clearSession(sessionId) {
1160
+ for (const [k, v] of this.#staged) if (v.sessionId === sessionId) this.#staged.delete(k);
1161
+ }
1162
+ };
1163
+ function installExitPlanValidator(ctx, bridge, requireOwnership, isEnabled = () => true) {
1164
+ return ctx.on("tools/pre-execute", async (exec, next) => {
1165
+ if (exec.name !== "exit_plan_mode") return next();
1166
+ if (!isEnabled(exec.agent)) return next();
1167
+ const agent = exec.agent;
1168
+ const sessionId = String(agent?.session?.id ?? "");
1169
+ if (!sessionId) return {
1170
+ kind: "deny",
1171
+ reason: "Plan Orchestrator: exit_plan_mode has no calling session."
1172
+ };
1173
+ try {
1174
+ const head = await fullHead(await repoRoot(agent.session.header?.cwd ?? process.cwd()));
1175
+ bridge.stage(sessionId, String(exec.callId ?? ""), String(exec.arguments?.plan ?? ""), head, requireOwnership());
1176
+ } catch (error) {
1177
+ return {
1178
+ kind: "deny",
1179
+ reason: `Plan Orchestrator: invalid executable PlanArtifact/preflight: ${error.message}`
1180
+ };
1181
+ }
1182
+ return next();
1183
+ });
1184
+ }
1185
+ /** rc.1 exit_plan_mode succeeds only with the structured { approved: true } output. */
1186
+ function isNativePlanApproved(result) {
1187
+ if (!result || typeof result !== "object") return false;
1188
+ const value = result;
1189
+ if (value.isError) return false;
1190
+ return typeof value.value === "object" && value.value !== null && value.value.approved === true;
1191
+ }
1192
+ /**
1193
+ * Every settled native exit consumes its staged candidate. Runtime disable is
1194
+ * checked before returning an approved artifact so toggling Enabled OFF while
1195
+ * the native approval UI is open can never produce a delayed handoff.
1196
+ */
1197
+ function consumeApprovedPlanResult(bridge, exec, result, isEnabled = () => true) {
1198
+ if (exec?.name !== "exit_plan_mode") return void 0;
1199
+ const sessionId = String(exec.agent?.session?.id ?? "");
1200
+ const callId = String(exec.callId ?? "");
1201
+ const staged = bridge.consume(sessionId, callId);
1202
+ if (!isEnabled(exec.agent) || !staged || !isNativePlanApproved(result)) return void 0;
1203
+ return staged;
1204
+ }
1205
+ //#endregion
1206
+ //#region src/planning/policy.ts
1207
+ const PLANNER_POLICY = `# Plan Orchestrator Contract
1208
+ You are the Planner. Plan mode is research-and-design only; do not mutate repository files.
1209
+
1210
+ ## Evidence before plan
1211
+ Classify claims as Verified Fact, Needs Verification, Recommendation, or Decision Lock. Never invent paths, APIs, commands, tests, dependencies, or runtime behavior.
1212
+
1213
+ ## Decisions and scope
1214
+ Prefer minimal sufficient architecture. No scope creep or speculative refactors. Ask the user only for product choices that cannot be discovered from the repository. A material unanswered choice blocks an executable plan. A Decision Lock may change only after new verified evidence and must record Previous Decision → New Evidence → Impact → Replacement Decision.
1215
+
1216
+ ## Tasks
1217
+ Split by responsibility, not arbitrary size. Every task has exact repository-relative modify[] ownership, acceptance, validation, dependsOn, and parallelSafe. Mark parallelSafe true only after checking shared APIs, schemas, types, config, migrations, generated output, mutable state, signatures, fixtures, and ordering; uncertainty means false.
1218
+
1219
+ ## Host validation safety
1220
+ validationCommands are not an arbitrary shell. Use only an existing package.json script through npm/pnpm/yarn/bun: \`<manager> test\` or \`<manager> run <script>\`; optional script arguments must follow \`--\`. Never emit curl/wget/npx, inline node/python/powershell, pipes, redirection, command substitution, chained shell commands, or an executable not represented by an existing package script.
1221
+
1222
+ ## Final gates
1223
+ Before exit_plan_mode: Scope, Evidence, Decision Stability, Task Boundaries, Add-When-Needed, Acceptance & Validation, Packet & Context Efficiency, Execution & Review Readiness must all pass.
1224
+ The Markdown plan MUST contain exactly one \`\`\`json fence with a strict PlanArtifact version 1. Do not emit an executable PlanArtifact if unresolved evidence can change architecture/ownership/user-visible behavior/acceptance.`;
1225
+ function researchPolicy(planning) {
1226
+ const lines = ["## Research budget"];
1227
+ if (planning.adaptiveResearch) lines.push("Research depth is adaptive: if evidence is sufficient, stop; otherwise identify the smallest missing evidence that can change architecture, ownership, user-visible behavior, or acceptance, verify only that gap, then reassess.");
1228
+ else lines.push("Adaptive research is disabled: perform one bounded verification pass and stop unless a missing fact makes an executable plan unsafe.");
1229
+ lines.push(`Initial read budget: at most ${planning.maxInitialReadFiles} files before reassessing evidence sufficiency.`);
1230
+ lines.push(`Soft Planner input budget: ${planning.softInputTokens} tokens. Treat this as a stop/reassess threshold, not permission to omit required evidence.`);
1231
+ if (planning.progressiveDiscovery) lines.push(planning.requireExpansionReason ? "Progressive discovery is allowed only for a concrete blocker; record the expansion reason before reading beyond the initial targets." : "Progressive discovery is allowed for evidence gaps, but expand incrementally and stop when the gap is closed.");
1232
+ else lines.push("Progressive discovery is disabled: do not expand beyond the initial read set; if evidence remains insufficient, report the blocker instead of guessing.");
1233
+ lines.push("No-progress research stops with an explicit missing-evidence report.");
1234
+ return lines.join("\n");
1235
+ }
1236
+ const COMPACT_PLANNER_REMINDER = "Plan Mode remains active: evidence-before-plan, Decision Locks, exact modify[] ownership, configured research budget, strict PlanArtifact v1, package-script-only host validation, no repository mutation.";
1237
+ function plannerPolicyText(enabled, active, first, planning) {
1238
+ if (!enabled || !active) return "";
1239
+ if (!first) return COMPACT_PLANNER_REMINDER;
1240
+ if (!planning) return PLANNER_POLICY;
1241
+ return `${PLANNER_POLICY}\n\n${researchPolicy(planning)}`;
1242
+ }
1243
+ //#endregion
1244
+ //#region src/planning/read-only-guard.ts
1245
+ const MUTATION_NAMES = /^(write|write_file|edit|edit_file|apply_patch|patch|delete|move|rename|mkdir|bash|pwsh|shell|run_code)$/i;
1246
+ var PlannerReadOnlyGuard = class {
1247
+ #active = /* @__PURE__ */ new Map();
1248
+ #degraded = /* @__PURE__ */ new Map();
1249
+ activate(session, sandboxPolicy) {
1250
+ const sid = String(session.id);
1251
+ if (this.#active.has(sid)) return;
1252
+ try {
1253
+ const previousEffective = sandboxPolicy.resolve({ session }).mode, previousOverride = sandboxPolicy.overrideOf(session);
1254
+ setSandboxMode(session, "read-only");
1255
+ const markerCount = this.modeEvents(session).length;
1256
+ this.#active.set(sid, {
1257
+ previousEffective,
1258
+ previousOverride,
1259
+ markerCount
1260
+ });
1261
+ } catch (e) {
1262
+ this.#degraded.set(sid, `sandbox override unavailable: ${e.message}`);
1263
+ this.#active.set(sid, {
1264
+ previousEffective: "read-only",
1265
+ markerCount: -1
1266
+ });
1267
+ }
1268
+ }
1269
+ deactivate(session, sandboxPolicy) {
1270
+ const sid = String(session.id), owned = this.#active.get(sid);
1271
+ if (!owned) return;
1272
+ this.#active.delete(sid);
1273
+ try {
1274
+ if (owned.markerCount < 0) return;
1275
+ const events = this.modeEvents(session);
1276
+ const last = events.at(-1);
1277
+ if (!(events.length === owned.markerCount && last?.data?.mode === "read-only")) return;
1278
+ const target = owned.previousOverride ?? owned.previousEffective;
1279
+ if (sandboxPolicy.resolve({ session }).mode === "read-only") setSandboxMode(session, target);
1280
+ } catch (e) {
1281
+ this.#degraded.set(sid, `sandbox restore skipped: ${e.message}`);
1282
+ }
1283
+ }
1284
+ active(sessionId) {
1285
+ return this.#active.has(sessionId);
1286
+ }
1287
+ degraded(sessionId) {
1288
+ return this.#degraded.get(sessionId);
1289
+ }
1290
+ markDegraded(sessionId, reason) {
1291
+ this.#degraded.set(sessionId, reason);
1292
+ }
1293
+ install(ctx, isEnabled = () => true) {
1294
+ return ctx.on("tools/pre-execute", async (exec, next) => {
1295
+ if (!isEnabled(exec.agent)) return next();
1296
+ const sid = String(exec.agent?.session?.id ?? "");
1297
+ if (!this.active(sid)) return next();
1298
+ if (MUTATION_NAMES.test(String(exec.name))) return {
1299
+ kind: "deny",
1300
+ reason: "Plan Mode is strict read-only; repository mutation is blocked until approval."
1301
+ };
1302
+ return next();
1303
+ });
1304
+ }
1305
+ modeEvents(session) {
1306
+ return (session.snapshotEvents?.() ?? []).filter((e) => e.type === "sandbox/mode");
1307
+ }
1308
+ };
1309
+ //#endregion
1310
+ //#region src/contract/events.ts
1311
+ function isPlanxEvent(event) {
1312
+ return Boolean(event && typeof event.type === "string" && event.type.startsWith("planx/") && event.data && typeof event.data.runId === "string");
1313
+ }
1314
+ function reducePlanxEvents(sessionId, events) {
1315
+ const rows = /* @__PURE__ */ new Map();
1316
+ for (const event of events) {
1317
+ if (!isPlanxEvent(event)) continue;
1318
+ const d = event.data;
1319
+ let v = rows.get(d.runId);
1320
+ if (event.type === "planx/run-approved") {
1321
+ v = {
1322
+ runId: d.runId,
1323
+ sessionId: String(d.sessionId ?? sessionId),
1324
+ phase: "APPROVED_PENDING",
1325
+ status: "APPROVED_PENDING",
1326
+ tasksTotal: d.tasksTotal,
1327
+ tasksDone: 0,
1328
+ activeTaskIds: [],
1329
+ reviewRound: 0,
1330
+ startedAt: d.at,
1331
+ updatedAt: d.at
1332
+ };
1333
+ rows.set(d.runId, v);
1334
+ continue;
1335
+ }
1336
+ if (!v) continue;
1337
+ if (event.type === "planx/run-phase") {
1338
+ v.phase = d.phase;
1339
+ v.status = d.phase;
1340
+ if (d.message) v.message = d.message;
1341
+ } else if (event.type === "planx/task-start") {
1342
+ if (!v.activeTaskIds.includes(d.taskId)) v.activeTaskIds.push(d.taskId);
1343
+ } else if (event.type === "planx/task-end") {
1344
+ v.activeTaskIds = v.activeTaskIds.filter((x) => x !== d.taskId);
1345
+ v.tasksDone = Math.min(v.tasksTotal, v.tasksDone + 1);
1346
+ } else if (event.type === "planx/review") v.reviewRound = d.round;
1347
+ else if (event.type === "planx/recovery") {
1348
+ v.status = d.status;
1349
+ if (d.message) v.message = d.message;
1350
+ } else if (event.type === "planx/run-terminal") {
1351
+ v.phase = d.phase;
1352
+ v.status = d.phase;
1353
+ v.activeTaskIds = [];
1354
+ if (d.message) v.message = d.message;
1355
+ }
1356
+ v.updatedAt = d.at;
1357
+ }
1358
+ return [...rows.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1359
+ }
1360
+ //#endregion
1361
+ //#region src/git/fingerprints.ts
1362
+ async function fingerprintPath(root, path) {
1363
+ const abs = join(root, path);
1364
+ try {
1365
+ const st = await lstat(abs);
1366
+ if (st.isSymbolicLink()) return {
1367
+ kind: "symlink",
1368
+ target: await readlink(abs)
1369
+ };
1370
+ if (!st.isFile()) throw new Error(`owned path is not a regular file: ${path}`);
1371
+ const bytes = await readFile(abs);
1372
+ return {
1373
+ kind: "file",
1374
+ sha256: createHash("sha256").update(bytes).digest("hex"),
1375
+ bytes: bytes.length
1376
+ };
1377
+ } catch (error) {
1378
+ if (error?.code === "ENOENT") return { kind: "missing" };
1379
+ throw error;
1380
+ }
1381
+ }
1382
+ async function snapshotDirty(root) {
1383
+ const head = await fullHead(root);
1384
+ const paths = {};
1385
+ for (const path of (await changedPaths(root)).sort()) paths[path] = await fingerprintPath(root, path);
1386
+ return {
1387
+ head,
1388
+ paths
1389
+ };
1390
+ }
1391
+ async function ownedFingerprint(root, paths) {
1392
+ const values = [];
1393
+ for (const path of [...new Set(paths)].sort()) values.push([path, await fingerprintPath(root, path)]);
1394
+ return createHash("sha256").update(JSON.stringify(values)).digest("hex");
1395
+ }
1396
+ function deltaPaths(before, after) {
1397
+ if (before.head !== after.head) throw new Error(`HEAD drift: ${before.head} -> ${after.head}`);
1398
+ return [.../* @__PURE__ */ new Set([...Object.keys(before.paths), ...Object.keys(after.paths)])].filter((key) => JSON.stringify(before.paths[key] ?? { kind: "clean" }) !== JSON.stringify(after.paths[key] ?? { kind: "clean" })).sort();
1399
+ }
1400
+ function snapshotHash(snapshot) {
1401
+ return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex");
1402
+ }
1403
+ //#endregion
1404
+ //#region src/recovery/reconcile.ts
1405
+ async function diagnoseResume(repo, manifest, checkpoint) {
1406
+ if (manifest.terminal) return {
1407
+ resumable: false,
1408
+ reason: "run is terminal"
1409
+ };
1410
+ if (await fullHead(repo) !== checkpoint.head) return {
1411
+ resumable: false,
1412
+ reason: "HEAD drift"
1413
+ };
1414
+ if (snapshotHash(await snapshotDirty(repo)) !== checkpoint.fingerprint) return {
1415
+ resumable: false,
1416
+ reason: "working tree fingerprint drift"
1417
+ };
1418
+ if (JSON.stringify([...manifest.ownership ?? []].sort()) !== JSON.stringify([...checkpoint.ownership].sort())) return {
1419
+ resumable: false,
1420
+ reason: "ownership checkpoint drift"
1421
+ };
1422
+ if (checkpoint.phase === "PREFLIGHT") return {
1423
+ resumable: true,
1424
+ reason: "checkpoint matches",
1425
+ resumeFrom: "PREFLIGHT",
1426
+ completedTaskIds: []
1427
+ };
1428
+ if (checkpoint.phase === "WORKERS" && checkpoint.safeBoundary) return {
1429
+ resumable: true,
1430
+ reason: "safe worker boundary matches",
1431
+ resumeFrom: "WORKERS",
1432
+ completedTaskIds: checkpoint.completedTaskIds ?? []
1433
+ };
1434
+ if (checkpoint.phase === "VALIDATING" || checkpoint.phase === "REVIEWING") return {
1435
+ resumable: true,
1436
+ reason: "final tree checkpoint matches",
1437
+ resumeFrom: "VALIDATING",
1438
+ completedTaskIds: checkpoint.completedTaskIds ?? []
1439
+ };
1440
+ return {
1441
+ resumable: false,
1442
+ reason: `phase ${checkpoint.phase} cannot be resumed without guessing mutation state`
1443
+ };
1444
+ }
1445
+ function interruptManifest(manifest) {
1446
+ if (!manifest.terminal) {
1447
+ manifest.phase = "INTERRUPTED";
1448
+ manifest.terminal = false;
1449
+ manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1450
+ }
1451
+ return manifest;
1452
+ }
1453
+ //#endregion
1454
+ //#region src/git/worktrees.ts
1455
+ function safeSegment(value) {
1456
+ if (/^[A-Za-z0-9._-]{1,120}$/.test(value)) return value;
1457
+ return createHash("sha256").update(value).digest("hex");
1458
+ }
1459
+ /** Engine lease ids may append -worktrees/-validation-* to the run UUID. All
1460
+ * physical leases still live under worktrees/<base-run-id>/ so terminal cleanup
1461
+ * owns one exact directory tree. */
1462
+ function worktreeRunKey(runId) {
1463
+ return safeSegment(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:-|$)/i.exec(runId)?.[1] ?? runId);
1464
+ }
1465
+ async function createWorktree(repo, runId, taskId, head, root = stateRoot()) {
1466
+ const runKey = worktreeRunKey(runId);
1467
+ const path = confined(root, "worktrees", runKey, safeSegment(`${runId}--${taskId}`));
1468
+ await mkdir(join(path, ".."), { recursive: true });
1469
+ await git(repo, [
1470
+ "worktree",
1471
+ "add",
1472
+ "--detach",
1473
+ path,
1474
+ head
1475
+ ]);
1476
+ return {
1477
+ path,
1478
+ runId: runKey,
1479
+ taskId,
1480
+ baseHead: head
1481
+ };
1482
+ }
1483
+ async function removeOwnedWorktree(repo, lease, force = false) {
1484
+ await git(repo, [
1485
+ "worktree",
1486
+ "remove",
1487
+ ...force ? ["--force"] : [],
1488
+ lease.path
1489
+ ]);
1490
+ await rm(lease.path, {
1491
+ recursive: true,
1492
+ force: true
1493
+ }).catch(() => {});
1494
+ }
1495
+ async function cleanupRunWorktrees(repo, runId, root = stateRoot()) {
1496
+ const runRoot = resolve(confined(root, "worktrees", worktreeRunKey(runId)));
1497
+ const listed = (await git(repo, [
1498
+ "worktree",
1499
+ "list",
1500
+ "--porcelain",
1501
+ "-z"
1502
+ ])).stdout.toString("utf8").split("\0");
1503
+ for (const rec of listed) {
1504
+ const line = rec.split("\n").find((x) => x.startsWith("worktree "));
1505
+ if (!line) continue;
1506
+ const path = resolve(line.slice(9));
1507
+ if (path !== runRoot && !path.startsWith(runRoot + sep)) continue;
1508
+ await git(repo, [
1509
+ "worktree",
1510
+ "remove",
1511
+ "--force",
1512
+ path
1513
+ ]).catch(() => {});
1514
+ await rm(path, {
1515
+ recursive: true,
1516
+ force: true
1517
+ }).catch(() => {});
1518
+ }
1519
+ await rm(runRoot, {
1520
+ recursive: true,
1521
+ force: true
1522
+ }).catch(() => {});
1523
+ }
1524
+ //#endregion
1525
+ //#region src/orchestration/service.ts
1526
+ var OrchestrationService = class {
1527
+ store;
1528
+ runner;
1529
+ #active = /* @__PURE__ */ new Map();
1530
+ #activeRun = /* @__PURE__ */ new Map();
1531
+ #pending = /* @__PURE__ */ new Map();
1532
+ #controllers = /* @__PURE__ */ new Map();
1533
+ #views = /* @__PURE__ */ new Map();
1534
+ #agents = /* @__PURE__ */ new Map();
1535
+ constructor(store, runner) {
1536
+ this.store = store;
1537
+ this.runner = runner;
1538
+ }
1539
+ approve(launch) {
1540
+ if (this.#pending.has(launch.sessionId) || this.#active.has(launch.sessionId)) return false;
1541
+ const runId = randomUUID();
1542
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1543
+ const view = {
1544
+ runId,
1545
+ sessionId: launch.sessionId,
1546
+ phase: "APPROVED_PENDING",
1547
+ status: "APPROVED_PENDING",
1548
+ tasksTotal: launch.artifact.tasks.length,
1549
+ tasksDone: 0,
1550
+ activeTaskIds: [],
1551
+ reviewRound: 0,
1552
+ startedAt: now,
1553
+ updatedAt: now
1554
+ };
1555
+ this.#views.set(runId, view);
1556
+ this.#agents.set(launch.sessionId, launch.agent);
1557
+ this.append(launch.agent.session, "planx/run-approved", {
1558
+ runId,
1559
+ sessionId: launch.sessionId,
1560
+ planHash: launch.planHash,
1561
+ tasksTotal: launch.artifact.tasks.length,
1562
+ at: now
1563
+ });
1564
+ const persisted = this.persistApproval(launch, runId, now);
1565
+ this.#pending.set(launch.sessionId, {
1566
+ runId,
1567
+ launch,
1568
+ persisted,
1569
+ cancelled: false
1570
+ });
1571
+ if (launch.agent.status === "idle") queueMicrotask(() => void this.onParentIdle(launch.sessionId));
1572
+ return runId;
1573
+ }
1574
+ shouldFence(sessionId) {
1575
+ return this.#pending.has(sessionId);
1576
+ }
1577
+ activeRun(sessionId) {
1578
+ return this.#pending.get(sessionId)?.runId ?? this.#activeRun.get(sessionId);
1579
+ }
1580
+ list(sessionId) {
1581
+ return [...this.#views.values()].filter((view) => !sessionId || view.sessionId === sessionId).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1582
+ }
1583
+ async detail(runId) {
1584
+ const view = this.#views.get(runId);
1585
+ if (!view) return void 0;
1586
+ const dir = this.store.runDir(view.sessionId, runId);
1587
+ const [completion, telemetry, worktrees, checkpoint] = await Promise.all([
1588
+ readJson(join(dir, "completion.json")).catch(() => void 0),
1589
+ readJson(join(dir, "telemetry.json")).catch(() => void 0),
1590
+ readJson(join(dir, "worktrees.json")).catch(() => void 0),
1591
+ readJson(join(dir, "recovery.json")).catch(() => void 0)
1592
+ ]);
1593
+ const latestReview = await this.latestReview(dir, view.reviewRound);
1594
+ return {
1595
+ ...view,
1596
+ ...completion,
1597
+ usage: completion?.usage ?? telemetry,
1598
+ review: latestReview?.verdict ?? completion?.review,
1599
+ worktrees: worktrees?.worktrees ?? [],
1600
+ recovery: checkpoint ? {
1601
+ phase: checkpoint.phase,
1602
+ safeBoundary: checkpoint.safeBoundary,
1603
+ completedTaskIds: checkpoint.completedTaskIds
1604
+ } : void 0
1605
+ };
1606
+ }
1607
+ recordEvent(kind, data) {
1608
+ const runId = String(data.runId ?? "");
1609
+ const view = this.#views.get(runId);
1610
+ if (!view) return;
1611
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1612
+ const session = this.#agents.get(view.sessionId)?.session;
1613
+ if (kind === "phase") {
1614
+ view.phase = String(data.phase);
1615
+ view.status = view.phase;
1616
+ view.reviewRound = Number(data.reviewRound ?? view.reviewRound);
1617
+ if (data.message) view.message = String(data.message);
1618
+ this.append(session, "planx/run-phase", {
1619
+ runId,
1620
+ phase: view.phase,
1621
+ message: data.message,
1622
+ reviewRound: view.reviewRound,
1623
+ at: now
1624
+ });
1625
+ } else if (kind === "task-start") {
1626
+ if (!view.activeTaskIds.includes(data.taskId)) view.activeTaskIds.push(data.taskId);
1627
+ this.append(session, "planx/task-start", {
1628
+ runId,
1629
+ taskId: String(data.taskId),
1630
+ at: now
1631
+ });
1632
+ } else if (kind === "task-end") {
1633
+ view.activeTaskIds = view.activeTaskIds.filter((id) => id !== data.taskId);
1634
+ view.tasksDone = Math.min(view.tasksTotal, view.tasksDone + 1);
1635
+ this.append(session, "planx/task-end", {
1636
+ runId,
1637
+ taskId: String(data.taskId),
1638
+ status: String(data.status ?? ""),
1639
+ at: now
1640
+ });
1641
+ } else if (kind === "validation") this.append(session, "planx/validation", {
1642
+ runId,
1643
+ commandId: String(data.commandId),
1644
+ status: String(data.status),
1645
+ at: now
1646
+ });
1647
+ else if (kind === "review") {
1648
+ view.reviewRound = Number(data.round ?? view.reviewRound);
1649
+ this.append(session, "planx/review", {
1650
+ runId,
1651
+ round: view.reviewRound,
1652
+ verdict: String(data.verdict),
1653
+ at: now
1654
+ });
1655
+ }
1656
+ view.updatedAt = now;
1657
+ }
1658
+ async cancel(runId, reason = "user requested stop") {
1659
+ const view = this.#views.get(runId);
1660
+ if (!view) return false;
1661
+ const sessionId = view.sessionId;
1662
+ const pending = this.#pending.get(sessionId);
1663
+ if (pending?.runId === runId) {
1664
+ pending.cancelled = true;
1665
+ let persistError;
1666
+ try {
1667
+ await pending.persisted;
1668
+ } catch (error) {
1669
+ persistError = error;
1670
+ }
1671
+ const controller = this.#controllers.get(runId);
1672
+ const active = this.#active.get(sessionId);
1673
+ if (controller && active) {
1674
+ if (!controller.signal.aborted) controller.abort(reason);
1675
+ await active.catch(() => {});
1676
+ } else await this.markCancelled(view, reason, persistError);
1677
+ if (this.#pending.get(sessionId) === pending) this.#pending.delete(sessionId);
1678
+ return true;
1679
+ }
1680
+ if (this.#activeRun.get(sessionId) !== runId) return false;
1681
+ const active = this.#active.get(sessionId);
1682
+ const controller = this.#controllers.get(runId);
1683
+ if (!active || !controller) return false;
1684
+ if (!controller.signal.aborted) controller.abort(reason);
1685
+ await active.catch(() => {});
1686
+ return true;
1687
+ }
1688
+ async cancelSession(sessionId, reason = "Plan Orchestrator disabled") {
1689
+ const runId = this.activeRun(sessionId);
1690
+ return runId ? this.cancel(runId, reason) : false;
1691
+ }
1692
+ async cancelAll(reason = "Plan Orchestrator disabled") {
1693
+ const sessionIds = /* @__PURE__ */ new Set([...this.#pending.keys(), ...this.#activeRun.keys()]);
1694
+ await Promise.allSettled([...sessionIds].map((sessionId) => this.cancelSession(sessionId, reason)));
1695
+ }
1696
+ async onParentIdle(sessionId) {
1697
+ const pending = this.#pending.get(sessionId);
1698
+ if (!pending || pending.cancelled || this.#active.has(sessionId)) return false;
1699
+ try {
1700
+ await pending.persisted;
1701
+ } catch (error) {
1702
+ if (this.#pending.get(sessionId) === pending) this.#pending.delete(sessionId);
1703
+ this.failView(pending.runId, "FAILED", `approval persistence failed: ${error.message}`);
1704
+ return false;
1705
+ }
1706
+ if (pending.cancelled || this.#pending.get(sessionId) !== pending) return false;
1707
+ const started = await this.start(pending.launch, pending.runId);
1708
+ if (pending.cancelled) {
1709
+ if (this.#pending.get(sessionId) === pending) this.#pending.delete(sessionId);
1710
+ return false;
1711
+ }
1712
+ if (this.#pending.get(sessionId) === pending) this.#pending.delete(sessionId);
1713
+ return started;
1714
+ }
1715
+ async reconcileSession(agent) {
1716
+ const sessionId = String(agent.session.id);
1717
+ this.#agents.set(sessionId, agent);
1718
+ for (const view of reducePlanxEvents(sessionId, agent.session.snapshotEvents?.() ?? [])) this.#views.set(view.runId, view);
1719
+ for (const original of await this.store.listSessionManifests(sessionId)) {
1720
+ if (original.terminal) continue;
1721
+ const manifest = interruptManifest(original);
1722
+ await this.store.writeManifest(manifest);
1723
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1724
+ const view = this.#views.get(manifest.runId) ?? {
1725
+ runId: manifest.runId,
1726
+ sessionId,
1727
+ phase: "INTERRUPTED",
1728
+ status: "INTERRUPTED",
1729
+ tasksTotal: 0,
1730
+ tasksDone: manifest.completedTaskIds?.length ?? 0,
1731
+ activeTaskIds: [],
1732
+ reviewRound: 0,
1733
+ startedAt: manifest.createdAt,
1734
+ updatedAt: now
1735
+ };
1736
+ view.phase = "INTERRUPTED";
1737
+ view.status = "INTERRUPTED";
1738
+ view.updatedAt = now;
1739
+ this.#views.set(manifest.runId, view);
1740
+ this.append(agent.session, "planx/recovery", {
1741
+ runId: manifest.runId,
1742
+ status: "INTERRUPTED",
1743
+ message: "Previous process ended before a terminal checkpoint.",
1744
+ at: now
1745
+ });
1746
+ }
1747
+ return this.list(sessionId);
1748
+ }
1749
+ async resume(runId) {
1750
+ const view = this.#views.get(runId);
1751
+ if (!view || view.phase !== "INTERRUPTED" || this.#active.has(view.sessionId) || this.#pending.has(view.sessionId)) return {
1752
+ ok: false,
1753
+ reason: "run is not resumable in current state"
1754
+ };
1755
+ const agent = this.#agents.get(view.sessionId);
1756
+ if (!agent) return {
1757
+ ok: false,
1758
+ reason: "session agent unavailable"
1759
+ };
1760
+ const manifest = await this.store.readManifest(view.sessionId, runId);
1761
+ const dir = this.store.runDir(view.sessionId, runId);
1762
+ const checkpoint = await readJson(join(dir, "recovery.json")).catch(() => void 0);
1763
+ if (!checkpoint) return {
1764
+ ok: false,
1765
+ reason: "recovery checkpoint missing"
1766
+ };
1767
+ const diagnosis = await diagnoseResume(await repoRoot(agent.session.header?.cwd ?? process.cwd()), manifest, checkpoint);
1768
+ if (!diagnosis.resumable) return {
1769
+ ok: false,
1770
+ reason: diagnosis.reason
1771
+ };
1772
+ const artifact = await readJson(join(dir, "plan.json"));
1773
+ const launch = {
1774
+ sessionId: view.sessionId,
1775
+ agent,
1776
+ artifact,
1777
+ planHash: manifest.planHash,
1778
+ baselineHead: manifest.baselineHead,
1779
+ resumeFrom: diagnosis.resumeFrom,
1780
+ completedTaskIds: diagnosis.completedTaskIds,
1781
+ externalIssue: manifest.externalIssue ? {
1782
+ ...manifest.externalIssue,
1783
+ publishAfterPass: Boolean(manifest.externalIssue.publishAfterPass)
1784
+ } : void 0
1785
+ };
1786
+ manifest.phase = diagnosis.resumeFrom;
1787
+ manifest.terminal = false;
1788
+ await this.store.writeManifest(manifest);
1789
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1790
+ this.append(agent.session, "planx/recovery", {
1791
+ runId,
1792
+ status: "RESUMING",
1793
+ message: `Safe resume from ${diagnosis.resumeFrom}`,
1794
+ at: now
1795
+ });
1796
+ await this.start(launch, runId);
1797
+ return {
1798
+ ok: true,
1799
+ resumeFrom: diagnosis.resumeFrom
1800
+ };
1801
+ }
1802
+ async cleanup(runId) {
1803
+ const view = this.#views.get(runId);
1804
+ if (!view || this.#active.has(view.sessionId)) return {
1805
+ ok: false,
1806
+ reason: "active or unknown run"
1807
+ };
1808
+ const manifest = await this.store.readManifest(view.sessionId, runId).catch(() => void 0);
1809
+ if (!manifest?.terminal) return {
1810
+ ok: false,
1811
+ reason: "only terminal runs may be cleaned"
1812
+ };
1813
+ if (manifest.repoRoot) await cleanupRunWorktrees(manifest.repoRoot, runId, this.store.root).catch(() => {});
1814
+ await this.store.removeRun(view.sessionId, runId);
1815
+ return { ok: true };
1816
+ }
1817
+ async start(launch, runId) {
1818
+ if (this.#active.has(launch.sessionId)) return false;
1819
+ const controller = new AbortController();
1820
+ this.#controllers.set(runId, controller);
1821
+ this.#activeRun.set(launch.sessionId, runId);
1822
+ let task;
1823
+ try {
1824
+ task = launch.agent.runMaintenance(async (signal) => this.run(launch, runId, AbortSignal.any([signal, controller.signal])));
1825
+ } catch (error) {
1826
+ this.#controllers.delete(runId);
1827
+ if (this.#activeRun.get(launch.sessionId) === runId) this.#activeRun.delete(launch.sessionId);
1828
+ throw error;
1829
+ }
1830
+ task = task.finally(() => {
1831
+ this.#active.delete(launch.sessionId);
1832
+ this.#controllers.delete(runId);
1833
+ if (this.#activeRun.get(launch.sessionId) === runId) this.#activeRun.delete(launch.sessionId);
1834
+ });
1835
+ this.#active.set(launch.sessionId, task);
1836
+ task.catch(() => {});
1837
+ return true;
1838
+ }
1839
+ async run(launch, runId, signal) {
1840
+ const initial = await this.store.readManifest(launch.sessionId, runId);
1841
+ initial.phase = launch.resumeFrom ?? "PREFLIGHT";
1842
+ initial.terminal = false;
1843
+ await this.store.writeManifest(initial);
1844
+ this.recordEvent("phase", {
1845
+ runId,
1846
+ phase: initial.phase
1847
+ });
1848
+ let terminalPhase = "COMPLETE";
1849
+ let terminalMessage;
1850
+ let thrown;
1851
+ try {
1852
+ await this.runner(launch, runId, signal);
1853
+ } catch (error) {
1854
+ thrown = error;
1855
+ terminalMessage = error.message;
1856
+ terminalPhase = signal.aborted ? "CANCELLED" : /(drift|ownership|blocked|inconclusive|unsafe|conflict|escape|checkpoint|tampered|stale)/i.test(terminalMessage) ? "BLOCKED" : "FAILED";
1857
+ }
1858
+ const latest = await this.store.readManifest(launch.sessionId, runId);
1859
+ latest.phase = terminalPhase;
1860
+ latest.terminal = true;
1861
+ await this.store.writeManifest(latest);
1862
+ this.recordEvent("phase", {
1863
+ runId,
1864
+ phase: terminalPhase,
1865
+ message: terminalMessage
1866
+ });
1867
+ this.append(launch.agent.session, "planx/run-terminal", {
1868
+ runId,
1869
+ phase: terminalPhase,
1870
+ ...terminalMessage ? { message: terminalMessage } : {},
1871
+ at: (/* @__PURE__ */ new Date()).toISOString()
1872
+ });
1873
+ if (thrown !== void 0) throw thrown;
1874
+ }
1875
+ async persistApproval(launch, runId, now) {
1876
+ const dir = this.store.runDir(launch.sessionId, runId);
1877
+ const manifest = {
1878
+ schemaVersion: 1,
1879
+ runId,
1880
+ sessionId: launch.sessionId,
1881
+ planHash: launch.planHash,
1882
+ phase: "APPROVED_PENDING",
1883
+ terminal: false,
1884
+ createdAt: now,
1885
+ updatedAt: now,
1886
+ baselineHead: launch.baselineHead,
1887
+ ownership: [...new Set(launch.artifact.tasks.flatMap((task) => task.modify))].sort(),
1888
+ completedTaskIds: [],
1889
+ externalIssue: launch.externalIssue ? {
1890
+ issueNumber: launch.externalIssue.issueNumber,
1891
+ repository: launch.externalIssue.repository,
1892
+ revision: launch.externalIssue.revision,
1893
+ branch: launch.externalIssue.branch,
1894
+ publishAfterPass: launch.externalIssue.publishAfterPass
1895
+ } : void 0,
1896
+ artifacts: {}
1897
+ };
1898
+ await this.store.writeManifest(manifest);
1899
+ await atomicJson(join(dir, "plan.json"), launch.artifact);
1900
+ }
1901
+ async markCancelled(view, reason, persistError) {
1902
+ view.phase = "CANCELLED";
1903
+ view.status = "CANCELLED";
1904
+ view.message = persistError ? `${reason}; approval persistence failed: ${persistError.message}` : reason;
1905
+ view.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1906
+ this.append(this.#agents.get(view.sessionId)?.session, "planx/run-terminal", {
1907
+ runId: view.runId,
1908
+ phase: "CANCELLED",
1909
+ message: view.message,
1910
+ at: view.updatedAt
1911
+ });
1912
+ const manifest = await this.store.readManifest(view.sessionId, view.runId).catch(() => void 0);
1913
+ if (manifest) {
1914
+ manifest.phase = "CANCELLED";
1915
+ manifest.terminal = true;
1916
+ await this.store.writeManifest(manifest);
1917
+ }
1918
+ }
1919
+ append(session, type, data) {
1920
+ if (!session || typeof session.append !== "function") throw new Error(`plan-orchestrator cannot persist ${type}: session append unavailable`);
1921
+ session.append(type, data);
1922
+ }
1923
+ failView(runId, phase, message) {
1924
+ const view = this.#views.get(runId);
1925
+ if (!view) return;
1926
+ view.phase = phase;
1927
+ view.status = phase;
1928
+ view.message = message;
1929
+ view.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1930
+ this.append(this.#agents.get(view.sessionId)?.session, "planx/run-terminal", {
1931
+ runId,
1932
+ phase,
1933
+ message,
1934
+ at: view.updatedAt
1935
+ });
1936
+ }
1937
+ async latestReview(dir, round) {
1938
+ for (let index = round; index >= 0; index--) {
1939
+ const value = await readJson(join(dir, `review-${index}.json`)).catch(() => void 0);
1940
+ if (value) return value;
1941
+ }
1942
+ }
1943
+ };
1944
+ //#endregion
1945
+ //#region src/orchestration/enabled-parent-fence.ts
1946
+ /**
1947
+ * Runtime-gated parent fence. Enabled OFF is a hard execution boundary: every
1948
+ * event re-checks settings, pending and active plugin-owned runs are cancelled,
1949
+ * then the native DSH flow is allowed through unchanged.
1950
+ */
1951
+ function installEnabledParentFence(ctx, service, isEnabled) {
1952
+ const cancelOwnedRun = async (agent) => {
1953
+ const sessionId = String(agent?.session?.id ?? "");
1954
+ if (!sessionId) return;
1955
+ await service.cancelSession(sessionId, "Plan Orchestrator disabled");
1956
+ };
1957
+ const preStep = ctx.on("agent/pre-step", async ({ agent }, next) => {
1958
+ if (!isEnabled(agent)) {
1959
+ await cancelOwnedRun(agent);
1960
+ return next();
1961
+ }
1962
+ const sessionId = String(agent.session.id);
1963
+ if (!service.shouldFence(sessionId)) return next();
1964
+ await next();
1965
+ return { kind: "reject" };
1966
+ }, { prepend: true });
1967
+ const status = ctx.on("agent/status", ({ agent, status }) => {
1968
+ if (!isEnabled(agent)) {
1969
+ cancelOwnedRun(agent).catch((error) => {
1970
+ ctx.logger?.warn?.("plan-orchestrator run cleanup after disable failed: %o", error);
1971
+ });
1972
+ return;
1973
+ }
1974
+ if (status === "idle") service.onParentIdle(String(agent.session.id)).catch((error) => {
1975
+ ctx.logger?.error?.("plan-orchestrator maintenance launch failed: %o", error);
1976
+ });
1977
+ });
1978
+ return () => {
1979
+ preStep?.();
1980
+ status?.();
1981
+ };
1982
+ }
1983
+ //#endregion
1984
+ //#region src/git/patches.ts
1985
+ async function capturePatch(root, baseHead, taskId, paths) {
1986
+ const files = [];
1987
+ for (const path of [...new Set(paths)].sort()) {
1988
+ const fp = await fingerprintPath(root, path);
1989
+ if (fp.kind === "missing") files.push({
1990
+ path,
1991
+ kind: "delete"
1992
+ });
1993
+ else if (fp.kind === "symlink") files.push({
1994
+ path,
1995
+ kind: "symlink",
1996
+ target: await readlink(join(root, path))
1997
+ });
1998
+ else {
1999
+ const b = await readFile(join(root, path));
2000
+ files.push({
2001
+ path,
2002
+ kind: "file",
2003
+ contentBase64: b.toString("base64"),
2004
+ sha256: createHash("sha256").update(b).digest("hex")
2005
+ });
2006
+ }
2007
+ }
2008
+ const raw = {
2009
+ version: 1,
2010
+ baseHead,
2011
+ taskId,
2012
+ files
2013
+ };
2014
+ return {
2015
+ ...raw,
2016
+ sha256: createHash("sha256").update(JSON.stringify(raw)).digest("hex")
2017
+ };
2018
+ }
2019
+ //#endregion
2020
+ //#region src/orchestration/integrator.ts
2021
+ function needsIntegrator(taskCount, hasWorktreePatch, crossTaskWiring, conflict) {
2022
+ return !(taskCount === 1 && !hasWorktreePatch && !crossTaskWiring && !conflict);
2023
+ }
2024
+ function verifyPatch(patch) {
2025
+ const { sha256, ...raw } = patch;
2026
+ if (createHash("sha256").update(JSON.stringify(raw)).digest("hex") !== sha256) throw new Error(`patch artifact hash mismatch for ${patch.taskId}`);
2027
+ for (const f of patch.files) if (f.kind === "file") {
2028
+ const bytes = Buffer.from(f.contentBase64 ?? "", "base64");
2029
+ if (createHash("sha256").update(bytes).digest("hex") !== f.sha256) throw new Error(`patch file hash mismatch: ${f.path}`);
2030
+ }
2031
+ }
2032
+ function assertSymlinkTarget(root, path, target) {
2033
+ if (!target) throw new Error(`empty symlink target: ${path}`);
2034
+ const b = resolve(root), resolved = resolve(dirname(join(b, path)), target);
2035
+ if (resolved !== b && !resolved.startsWith(b + sep)) throw new Error(`symlink target escapes repository: ${path} -> ${target}`);
2036
+ }
2037
+ async function applyPatchArtifact(root, patch, unionOwnership) {
2038
+ verifyPatch(patch);
2039
+ const paths = patch.files.map((f) => f.path);
2040
+ assertOwnedPaths(paths, unionOwnership);
2041
+ await assertRepoPathsConfined(root, paths);
2042
+ for (const f of patch.files) {
2043
+ const target = join(root, f.path);
2044
+ if (f.kind === "delete") {
2045
+ await rm(target, { force: true });
2046
+ continue;
2047
+ }
2048
+ await mkdir(dirname(target), { recursive: true });
2049
+ if (f.kind === "symlink") {
2050
+ assertSymlinkTarget(root, f.path, f.target);
2051
+ await rm(target, { force: true });
2052
+ await symlink(f.target, target);
2053
+ continue;
2054
+ }
2055
+ await writeFile(target, Buffer.from(f.contentBase64, "base64"));
2056
+ }
2057
+ }
2058
+ function integratorPrompt(planJson, handoffs, patchLocators) {
2059
+ return `Role: Integrator\nDo not redesign or expand scope. You may modify only union ownership. Account for every accepted Worker patch; missing/ambiguous artifacts are BLOCKED. Never commit/push/reset/clean.\n\nAuthoritative PlanArtifact:\n${planJson}\n\nBounded handoff:\n${handoffs.join("\n")}\n\nPatch locators:\n${patchLocators.join("\n")}`;
2060
+ }
2061
+ //#endregion
2062
+ //#region src/orchestration/scheduler.ts
2063
+ function buildWaves(plan, maxParallel = 3, mode = "auto", platform = process.platform) {
2064
+ new Map(plan.tasks.map((t) => [t.id, t]));
2065
+ const done = /* @__PURE__ */ new Set(), remaining = new Set(plan.tasks.map((t) => t.id)), waves = [];
2066
+ while (remaining.size) {
2067
+ const ready = plan.tasks.filter((t) => remaining.has(t.id) && t.dependsOn.every((d) => done.has(d)));
2068
+ if (!ready.length) throw new Error("scheduler deadlock");
2069
+ if (mode !== "serial" && maxParallel > 1) {
2070
+ const candidates = ready.filter((t) => t.parallelSafe);
2071
+ const selected = [];
2072
+ for (const t of candidates) {
2073
+ if (selected.length >= maxParallel) break;
2074
+ if (selected.every((s) => disjointOwnership(s.modify, t.modify, platform))) selected.push(t);
2075
+ }
2076
+ if (selected.length >= 2) {
2077
+ const ids = selected.map((t) => t.id);
2078
+ waves.push({
2079
+ mode: "parallel",
2080
+ taskIds: ids
2081
+ });
2082
+ for (const id of ids) {
2083
+ remaining.delete(id);
2084
+ done.add(id);
2085
+ }
2086
+ continue;
2087
+ }
2088
+ }
2089
+ const t = ready[0];
2090
+ waves.push({
2091
+ mode: "serial",
2092
+ taskIds: [t.id]
2093
+ });
2094
+ remaining.delete(t.id);
2095
+ done.add(t.id);
2096
+ }
2097
+ return waves;
2098
+ }
2099
+ function recheckWave(plan, wave, platform = process.platform) {
2100
+ if (wave.mode === "serial") return true;
2101
+ const tasks = wave.taskIds.map((id) => plan.tasks.find((t) => t.id === id));
2102
+ return tasks.length >= 2 && tasks.every((t) => t.parallelSafe) && tasks.every((t, i) => tasks.slice(i + 1).every((o) => disjointOwnership(t.modify, o.modify, platform))) && tasks.every((t) => t.dependsOn.every((d) => !wave.taskIds.includes(d)));
2103
+ }
2104
+ //#endregion
2105
+ //#region src/orchestration/task-packet.ts
2106
+ function buildTaskPacket(plan, task, dependencyHandoff = []) {
2107
+ return [
2108
+ `Role: Worker`,
2109
+ `Task ID: ${task.id}`,
2110
+ `Objective: ${task.objective}`,
2111
+ `Global Decision Locks:\n${plan.decisionLocks.map((x) => `- ${x}`).join("\n") || "- none"}`,
2112
+ `Task Decision Locks:\n${task.decisionLocks.map((x) => `- ${x}`).join("\n") || "- none"}`,
2113
+ `Prioritized read targets (start here; expand only for a concrete blocker):\n${task.read.map((x) => `- ${x}`).join("\n") || "- none"}`,
2114
+ `Exact modify ownership (MUST NOT mutate outside):\n${task.modify.map((x) => `- ${x}`).join("\n")}`,
2115
+ `Required changes:\n${task.requiredChanges.map((x) => `- ${x}`).join("\n")}`,
2116
+ `Acceptance criteria:\n${task.acceptanceCriteria.map((x) => `- ${x}`).join("\n")}`,
2117
+ `Validation requested:\n${task.validation.map((x) => `- ${x}`).join("\n") || "- host validation follows"}`,
2118
+ `Dependency handoff:\n${dependencyHandoff.map((x) => `- ${x}`).join("\n") || "- none"}`,
2119
+ `Context expansion: if you need files outside initial read targets, record a concrete blocker and expansion reason. Do not read the Planner conversation.`,
2120
+ `Return only the configured structured completion contract.`
2121
+ ].join("\n\n");
2122
+ }
2123
+ //#endregion
2124
+ //#region src/contract/role-result.ts
2125
+ const ROLE_RESULT_SCHEMA = {
2126
+ type: "object",
2127
+ additionalProperties: false,
2128
+ properties: {
2129
+ taskId: {
2130
+ type: "string",
2131
+ maxLength: 200
2132
+ },
2133
+ status: {
2134
+ type: "string",
2135
+ enum: [
2136
+ "COMPLETE",
2137
+ "BLOCKED",
2138
+ "FAILED"
2139
+ ]
2140
+ },
2141
+ changed: {
2142
+ type: "array",
2143
+ items: {
2144
+ type: "string",
2145
+ maxLength: 512
2146
+ },
2147
+ maxItems: 200
2148
+ },
2149
+ validation: {
2150
+ type: "array",
2151
+ items: {
2152
+ type: "object",
2153
+ additionalProperties: false,
2154
+ properties: {
2155
+ id: {
2156
+ type: "string",
2157
+ maxLength: 200
2158
+ },
2159
+ status: {
2160
+ type: "string",
2161
+ enum: [
2162
+ "PASS",
2163
+ "FAIL",
2164
+ "INCONCLUSIVE"
2165
+ ]
2166
+ },
2167
+ detail: {
2168
+ type: "string",
2169
+ maxLength: 2e3
2170
+ }
2171
+ },
2172
+ required: ["id", "status"]
2173
+ },
2174
+ maxItems: 30
2175
+ },
2176
+ remaining: {
2177
+ type: "array",
2178
+ items: {
2179
+ type: "string",
2180
+ maxLength: 1e3
2181
+ },
2182
+ maxItems: 30
2183
+ },
2184
+ contextExpansion: {
2185
+ type: "array",
2186
+ items: {
2187
+ type: "string",
2188
+ maxLength: 1e3
2189
+ },
2190
+ maxItems: 30
2191
+ }
2192
+ },
2193
+ required: [
2194
+ "taskId",
2195
+ "status",
2196
+ "changed",
2197
+ "validation",
2198
+ "remaining",
2199
+ "contextExpansion"
2200
+ ]
2201
+ };
2202
+ const KEYS = /* @__PURE__ */ new Set([
2203
+ "taskId",
2204
+ "status",
2205
+ "changed",
2206
+ "validation",
2207
+ "remaining",
2208
+ "contextExpansion"
2209
+ ]);
2210
+ const VKEYS = /* @__PURE__ */ new Set([
2211
+ "id",
2212
+ "status",
2213
+ "detail"
2214
+ ]);
2215
+ function boundedStrings(value, name, maxItems, maxChars) {
2216
+ if (!Array.isArray(value) || value.length > maxItems) throw new Error(`${name} invalid`);
2217
+ for (const item of value) if (typeof item !== "string" || item.length > maxChars) throw new Error(`${name} invalid`);
2218
+ return value;
2219
+ }
2220
+ function validateRoleResult(value, taskId) {
2221
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("role result must be an object");
2222
+ const x = value;
2223
+ const unknown = Object.keys(x).filter((k) => !KEYS.has(k));
2224
+ if (unknown.length) throw new Error(`role result unknown fields: ${unknown.join(", ")}`);
2225
+ if (x.taskId !== taskId || typeof x.taskId !== "string" || x.taskId.length > 200) throw new Error("role taskId mismatch");
2226
+ if (![
2227
+ "COMPLETE",
2228
+ "BLOCKED",
2229
+ "FAILED"
2230
+ ].includes(String(x.status))) throw new Error("role status invalid");
2231
+ boundedStrings(x.changed, "changed", 200, 512);
2232
+ boundedStrings(x.remaining, "remaining", 30, 1e3);
2233
+ boundedStrings(x.contextExpansion, "contextExpansion", 30, 1e3);
2234
+ if (!Array.isArray(x.validation) || x.validation.length > 30) throw new Error("validation invalid");
2235
+ for (const item of x.validation) {
2236
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("validation invalid");
2237
+ const v = item;
2238
+ if (Object.keys(v).some((k) => !VKEYS.has(k)) || typeof v.id !== "string" || v.id.length > 200 || ![
2239
+ "PASS",
2240
+ "FAIL",
2241
+ "INCONCLUSIVE"
2242
+ ].includes(String(v.status)) || v.detail !== void 0 && (typeof v.detail !== "string" || v.detail.length > 2e3)) throw new Error("validation invalid");
2243
+ }
2244
+ const encoded = JSON.stringify(value);
2245
+ if (Buffer.byteLength(encoded, "utf8") > 128 * 1024) throw new Error("role result exceeds 128KiB");
2246
+ return value;
2247
+ }
2248
+ //#endregion
2249
+ //#region src/telemetry/usage.ts
2250
+ const num = (v) => Number.isFinite(v) ? Number(v) : void 0;
2251
+ function usageFromSession(ctx, role, session, route) {
2252
+ try {
2253
+ const state = ctx.sessionProjections?.stateOf?.(session, "tokenUsage");
2254
+ const totals = state?.totals ?? state;
2255
+ const uncached = num(totals?.uncachedInputTokens), read = num(totals?.cacheReadTokens), write = num(totals?.cacheWriteTokens), output = num(totals?.outputTokens);
2256
+ let pressure;
2257
+ try {
2258
+ pressure = num(ctx.get?.("tokenMeter")?.measure?.(session)?.totalTokens);
2259
+ } catch {}
2260
+ if ([
2261
+ uncached,
2262
+ read,
2263
+ write,
2264
+ output
2265
+ ].every((v) => typeof v === "number")) return {
2266
+ role,
2267
+ uncachedInput: uncached,
2268
+ cacheRead: read,
2269
+ cacheWrite: write,
2270
+ output,
2271
+ input: uncached + read + write,
2272
+ provider: route?.provider,
2273
+ model: route?.model,
2274
+ pressureTokens: pressure,
2275
+ source: "dsh-token-projection"
2276
+ };
2277
+ } catch {}
2278
+ return {
2279
+ role,
2280
+ provider: route?.provider,
2281
+ model: route?.model,
2282
+ source: "unavailable"
2283
+ };
2284
+ }
2285
+ function usageInEvent(event) {
2286
+ if (event?.type === "assistant/message" && event.data?.usage) return event.data.usage;
2287
+ const stream = event?.data?.stream;
2288
+ if (!Array.isArray(stream)) return void 0;
2289
+ for (let i = stream.length - 1; i >= 0; i--) {
2290
+ const u = stream[i]?.usage ?? stream[i]?.chunk?.usage;
2291
+ if (u) return u;
2292
+ }
2293
+ }
2294
+ function usageFromSdkEvents(role, events, route) {
2295
+ let uncached = 0, read = 0, write = 0, output = 0, samples = 0;
2296
+ const seen = /* @__PURE__ */ new Map();
2297
+ for (const event of events) {
2298
+ if (event?.type !== "assistant/message" && event?.type !== "assistant/attempt" && event?.type !== "llm/retry-started") continue;
2299
+ if (event.type === "llm/retry-started") {
2300
+ seen.delete(`${event.data?.turn}:${event.data?.step}`);
2301
+ continue;
2302
+ }
2303
+ const u = usageInEvent(event);
2304
+ if (!u) continue;
2305
+ const key = `${event.data?.turn}:${event.data?.step}`, next = {
2306
+ uncached: Number(u.inputTokens ?? 0),
2307
+ read: Number(u.cacheReadTokens ?? 0),
2308
+ write: Number(u.cacheWriteTokens ?? 0),
2309
+ output: Number(u.outputTokens ?? 0)
2310
+ }, prev = seen.get(key);
2311
+ if (prev) {
2312
+ uncached -= prev.uncached;
2313
+ read -= prev.read;
2314
+ write -= prev.write;
2315
+ output -= prev.output;
2316
+ } else samples++;
2317
+ uncached += next.uncached;
2318
+ read += next.read;
2319
+ write += next.write;
2320
+ output += next.output;
2321
+ seen.set(key, next);
2322
+ }
2323
+ return samples ? {
2324
+ role,
2325
+ uncachedInput: uncached,
2326
+ cacheRead: read,
2327
+ cacheWrite: write,
2328
+ output,
2329
+ input: uncached + read + write,
2330
+ turns: samples,
2331
+ provider: route?.provider,
2332
+ model: route?.model,
2333
+ source: "sdk-events"
2334
+ } : {
2335
+ role,
2336
+ provider: route?.provider,
2337
+ model: route?.model,
2338
+ source: "unavailable"
2339
+ };
2340
+ }
2341
+ //#endregion
2342
+ //#region src/runtime-policy.ts
2343
+ let timeoutResolver = () => 9e5;
2344
+ function configureRoleTimeoutResolver(resolver) {
2345
+ const previous = timeoutResolver;
2346
+ timeoutResolver = resolver;
2347
+ return () => {
2348
+ if (timeoutResolver === resolver) timeoutResolver = previous;
2349
+ };
2350
+ }
2351
+ function roleTimeoutMs(cwd) {
2352
+ const value = timeoutResolver(cwd);
2353
+ if (!Number.isSafeInteger(value) || value < 1e3 || value > 36e5) throw new Error("invalid Plan Orchestrator role timeout");
2354
+ return value;
2355
+ }
2356
+ function withRoleTimeout(cwd, signal) {
2357
+ return AbortSignal.any([signal, AbortSignal.timeout(roleTimeoutMs(cwd))]);
2358
+ }
2359
+ //#endregion
2360
+ //#region src/orchestration/ownership-guard.ts
2361
+ const FILE_MUTATORS = /^(?:write|write_file|edit|edit_file|delete|delete_file|move|move_file|rename|rename_file|mkdir|make_directory|apply_patch|patch)$/i;
2362
+ const PATH_KEYS = /* @__PURE__ */ new Set([
2363
+ "path",
2364
+ "file",
2365
+ "filePath",
2366
+ "file_path",
2367
+ "target",
2368
+ "destination",
2369
+ "dest",
2370
+ "to",
2371
+ "source",
2372
+ "src",
2373
+ "from",
2374
+ "newPath",
2375
+ "new_path",
2376
+ "oldPath",
2377
+ "old_path"
2378
+ ]);
2379
+ function patchPaths(text) {
2380
+ const out = [];
2381
+ for (const line of text.split(/\r?\n/)) {
2382
+ let match = /^\*\*\* (?:Update|Delete|Add) File:\s+(.+?)\s*$/.exec(line);
2383
+ if (!match) match = /^(?:\+\+\+|---)\s+(?:[ab]\/)?(.+?)\s*$/.exec(line);
2384
+ const value = match?.[1];
2385
+ if (value && value !== "/dev/null") out.push(value);
2386
+ }
2387
+ return out;
2388
+ }
2389
+ function mutationTargetCandidates(toolName, args) {
2390
+ if (!FILE_MUTATORS.test(toolName)) return void 0;
2391
+ if (!args || typeof args !== "object" || Array.isArray(args)) return [];
2392
+ const record = args;
2393
+ const found = [];
2394
+ for (const [key, value] of Object.entries(record)) {
2395
+ if (PATH_KEYS.has(key)) {
2396
+ if (typeof value === "string") found.push(value);
2397
+ else if (Array.isArray(value)) found.push(...value.filter((item) => typeof item === "string"));
2398
+ }
2399
+ if ((key === "patch" || key === "diff" || key === "input") && typeof value === "string") found.push(...patchPaths(value));
2400
+ }
2401
+ return [...new Set(found.filter(Boolean))];
2402
+ }
2403
+ function toolPathToRepoRelative(root, raw) {
2404
+ if (raw.includes("\0")) throw new Error("tool target contains NUL");
2405
+ const absolute = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
2406
+ const rel = relative(resolve(root), absolute);
2407
+ if (!rel || rel === ".") throw new Error(`tool target is not an exact file: ${raw}`);
2408
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error(`tool target escapes repository: ${raw}`);
2409
+ return normalizeOwnedPath(rel.split(sep).join("/"));
2410
+ }
2411
+ function ownershipGuardReason(root, allowed, toolName, args) {
2412
+ const candidates = mutationTargetCandidates(toolName, args);
2413
+ if (candidates === void 0) return void 0;
2414
+ if (candidates.length === 0) return `Plan Orchestrator ownership guard cannot identify target path for mutating tool ${toolName}`;
2415
+ const allowedSet = new Set(allowed.map((path) => schedulerPathIdentity(path)));
2416
+ try {
2417
+ for (const raw of candidates) {
2418
+ const relativePath = toolPathToRepoRelative(root, raw);
2419
+ if (!allowedSet.has(schedulerPathIdentity(relativePath))) return `Plan Orchestrator ownership violation: ${relativePath}`;
2420
+ }
2421
+ } catch (error) {
2422
+ return `Plan Orchestrator ownership violation: ${error.message}`;
2423
+ }
2424
+ }
2425
+ /**
2426
+ * Install a monotonic guard before the one-shot child is created. A local
2427
+ * one-shot child records its direct parent in SessionHeader.parentSession, so
2428
+ * the guard can protect even the first tool call without waiting for start()
2429
+ * to return the child handle.
2430
+ */
2431
+ function installChildOwnershipGuard(ctx, parent, root, allowed) {
2432
+ if (!ctx.tools?.guard) throw new Error("tools.guard unavailable for exact ownership enforcement");
2433
+ const parentId = String(parent.session.id);
2434
+ return ctx.tools.guard((exec) => {
2435
+ const agent = exec.agent;
2436
+ if (!agent || String(agent.session?.header?.parentSession ?? "") !== parentId || agent.session?.header?.origin !== "subagent") return void 0;
2437
+ return ownershipGuardReason(root, allowed, String(exec.name ?? ""), exec.arguments);
2438
+ });
2439
+ }
2440
+ //#endregion
2441
+ //#region src/orchestration/native-backend.ts
2442
+ const OWNERSHIP_SAFE_MUTATION_TOOLS = [
2443
+ "read",
2444
+ "glob",
2445
+ "grep",
2446
+ "lsp",
2447
+ "web_search",
2448
+ "web_fetch",
2449
+ "write",
2450
+ "write_file",
2451
+ "edit",
2452
+ "edit_file",
2453
+ "delete",
2454
+ "delete_file",
2455
+ "move",
2456
+ "move_file",
2457
+ "rename",
2458
+ "rename_file",
2459
+ "apply_patch",
2460
+ "patch"
2461
+ ];
2462
+ var NativeSpawnBackend = class {
2463
+ ctx;
2464
+ constructor(ctx) {
2465
+ this.ctx = ctx;
2466
+ }
2467
+ async run(req) {
2468
+ const started = Date.now();
2469
+ const release = req.ownership ? installChildOwnershipGuard(this.ctx, req.parent, req.ownership.root, req.ownership.paths) : () => {};
2470
+ let run;
2471
+ try {
2472
+ const cwd = req.parent?.session?.header?.cwd;
2473
+ const signal = req.ownership ? withRoleTimeout(cwd, req.signal) : req.signal;
2474
+ const toolFilter = req.ownership ? { allow: OWNERSHIP_SAFE_MUTATION_TOOLS } : req.toolFilter;
2475
+ run = await this.ctx.subagents.start("spawn", {
2476
+ label: `${req.role}:${req.taskId}`,
2477
+ prompt: [{
2478
+ type: "text",
2479
+ text: req.prompt
2480
+ }],
2481
+ parent: req.parent,
2482
+ signal,
2483
+ agentOptions: req.route,
2484
+ outputSchema: ROLE_RESULT_SCHEMA,
2485
+ maxDepth: 1,
2486
+ toolFilter,
2487
+ persona: req.persona
2488
+ });
2489
+ const result = await run.result;
2490
+ if (result.stopReason !== "completed") throw new Error(`subagent ${req.taskId} stopped: ${result.stopReason}${result.diagnostic ? `: ${result.diagnostic}` : ""}`);
2491
+ const valid = validateRoleResult(result.structured, req.taskId);
2492
+ const usage = run.localAgent ? usageFromSession(this.ctx, req.role, run.localAgent, req.route) : {
2493
+ role: req.role,
2494
+ provider: req.route.provider,
2495
+ model: req.route.model,
2496
+ source: "unavailable"
2497
+ };
2498
+ usage.durationMs = Date.now() - started;
2499
+ return {
2500
+ ...valid,
2501
+ __usage: usage
2502
+ };
2503
+ } finally {
2504
+ try {
2505
+ if (run) await run.dispose();
2506
+ } finally {
2507
+ release();
2508
+ }
2509
+ }
2510
+ }
2511
+ };
2512
+ //#endregion
2513
+ //#region src/orchestration/sdk-backend.ts
2514
+ function extractEnvelope(text) {
2515
+ const value = text.trim();
2516
+ if (!value.startsWith("{") || !value.endsWith("}")) throw new Error("SDK worker must return one strict JSON envelope");
2517
+ const parsed = JSON.parse(value);
2518
+ if (JSON.stringify(parsed).length > 128 * 1024) throw new Error("SDK worker JSON envelope exceeds 128KiB");
2519
+ return parsed;
2520
+ }
2521
+ /**
2522
+ * Keep only process/runtime facts plus provider credentials likely required by
2523
+ * the selected route. This intentionally does not serialize or persist the env.
2524
+ */
2525
+ function minimalSdkEnv(source = process.env) {
2526
+ const exact = /* @__PURE__ */ new Set([
2527
+ "PATH",
2528
+ "Path",
2529
+ "PATHEXT",
2530
+ "SystemRoot",
2531
+ "SYSTEMROOT",
2532
+ "COMSPEC",
2533
+ "HOME",
2534
+ "USERPROFILE",
2535
+ "APPDATA",
2536
+ "LOCALAPPDATA",
2537
+ "TEMP",
2538
+ "TMP",
2539
+ "TMPDIR",
2540
+ "LANG",
2541
+ "LC_ALL",
2542
+ "NODE_OPTIONS",
2543
+ "HTTPS_PROXY",
2544
+ "HTTP_PROXY",
2545
+ "NO_PROXY",
2546
+ "SSL_CERT_FILE",
2547
+ "SSL_CERT_DIR",
2548
+ "DSH_HOME"
2549
+ ]);
2550
+ const prefix = /^(DSH_|OPENAI_|DEEPSEEK_|ANTHROPIC_|GOOGLE_|GEMINI_|COMMANDCODE_|CODEX_|AZURE_)/i;
2551
+ const env = {};
2552
+ for (const [key, value] of Object.entries(source)) if (value !== void 0 && (exact.has(key) || prefix.test(key))) env[key] = value;
2553
+ return env;
2554
+ }
2555
+ function packagedProfilePath(role) {
2556
+ const here = dirname(fileURLToPath(import.meta.url));
2557
+ return join(basename(here) === "lib" ? resolve(here, "..") : resolve(here, "../.."), "profiles", `${role}.cordis.yml`);
2558
+ }
2559
+ function sdkHarnessOptions(req) {
2560
+ const patches = req.patches ?? [packagedProfilePath(req.role ?? "worker")];
2561
+ return {
2562
+ profile: req.profile,
2563
+ patches,
2564
+ cwd: req.cwd,
2565
+ processCwd: req.cwd,
2566
+ provider: req.route.provider,
2567
+ model: req.route.model,
2568
+ reasoningEffort: req.route.reasoningEffort,
2569
+ maxTokens: req.route.maxTokens,
2570
+ env: req.env ?? minimalSdkEnv()
2571
+ };
2572
+ }
2573
+ var SdkWorkspaceBackend = class {
2574
+ async run(req) {
2575
+ const signal = withRoleTimeout(req.cwd, req.signal);
2576
+ signal.throwIfAborted();
2577
+ const { DeepSeekHarness } = await import("@deepseek-ai/dsh-sdk-client");
2578
+ const harness = new DeepSeekHarness(sdkHarnessOptions(req));
2579
+ const started = Date.now();
2580
+ let abortClose;
2581
+ const abortPromise = new Promise((_, reject) => {
2582
+ const onAbort = () => {
2583
+ harness.close().catch(() => {});
2584
+ reject(new DOMException("SDK worker aborted", "AbortError"));
2585
+ };
2586
+ signal.addEventListener("abort", onAbort, { once: true });
2587
+ abortClose = () => signal.removeEventListener("abort", onAbort);
2588
+ });
2589
+ try {
2590
+ const result = await Promise.race([harness.run(req.prompt), abortPromise]);
2591
+ const valid = validateRoleResult(extractEnvelope(result.finalResponse), req.taskId);
2592
+ const usage = usageFromSdkEvents("worker", result.events, req.route);
2593
+ usage.durationMs = Date.now() - started;
2594
+ return {
2595
+ ...valid,
2596
+ __usage: usage
2597
+ };
2598
+ } finally {
2599
+ abortClose?.();
2600
+ await harness.close().catch(() => {});
2601
+ }
2602
+ }
2603
+ };
2604
+ //#endregion
2605
+ //#region src/orchestration/role-router.ts
2606
+ function routeChoices(route, current) {
2607
+ return [route.mode === "current" ? current : {
2608
+ provider: route.provider,
2609
+ model: route.model,
2610
+ reasoningEffort: route.reasoningEffort,
2611
+ maxTokens: route.maxTokens
2612
+ }, ...route.fallbacks].filter((v, i, a) => v.provider && v.model && a.findIndex((x) => x.provider === v.provider && x.model === v.model && x.reasoningEffort === v.reasoningEffort) === i);
2613
+ }
2614
+ async function preflightRoute(llm, choice) {
2615
+ if (typeof llm?.resolveCallConfig !== "function") throw new Error("llm.resolveCallConfig unavailable");
2616
+ await llm.resolveCallConfig(choice);
2617
+ return choice;
2618
+ }
2619
+ //#endregion
2620
+ //#region src/validation/runner.ts
2621
+ let configuredShell;
2622
+ /** Bind validation to the current DSH shell seam. There is deliberately no
2623
+ * child_process fallback: missing sandboxed shell execution is release-blocking. */
2624
+ function configureValidationShell(shell) {
2625
+ const previous = configuredShell;
2626
+ configuredShell = shell;
2627
+ return () => {
2628
+ if (configuredShell === shell) configuredShell = previous;
2629
+ };
2630
+ }
2631
+ function safe(value) {
2632
+ return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 160);
2633
+ }
2634
+ async function assertExistingPackageScript(cwd, command) {
2635
+ const parsed = parseValidationCommand(command);
2636
+ let pkg;
2637
+ try {
2638
+ pkg = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
2639
+ } catch (error) {
2640
+ throw new Error(`validation requires readable package.json: ${error.message}`);
2641
+ }
2642
+ if (!pkg?.scripts || typeof pkg.scripts[parsed.script] !== "string" || !pkg.scripts[parsed.script].trim()) throw new Error(`validation package script does not exist: ${parsed.script}`);
2643
+ }
2644
+ async function runValidation(opts) {
2645
+ const shell = opts.shell ?? configuredShell;
2646
+ if (!shell?.resolve || !shell?.run) throw new Error("sandboxed DSH shell executor unavailable for host validation");
2647
+ await assertExistingPackageScript(opts.cwd, opts.command);
2648
+ const before = await snapshotDirty(opts.cwd);
2649
+ const head = await fullHead(opts.cwd);
2650
+ const start = (/* @__PURE__ */ new Date()).toISOString();
2651
+ const sandboxPolicy = {
2652
+ mode: "workspace-write",
2653
+ workspaceRoot: opts.cwd,
2654
+ ...opts.sessionId ? { sessionId: opts.sessionId } : {}
2655
+ };
2656
+ const spec = shell.resolve({
2657
+ command: opts.command,
2658
+ workdir: opts.cwd,
2659
+ timeoutMs: opts.timeoutMs,
2660
+ stdoutMaxBytes: opts.capBytes,
2661
+ sandboxPolicy
2662
+ });
2663
+ const result = await shell.run(spec);
2664
+ const stdout = Buffer.from(String(result?.stdout?.text ?? ""), "utf8");
2665
+ const stderr = Buffer.from(String(result?.stderr?.text ?? ""), "utf8");
2666
+ const stdoutTruncated = Boolean(result?.stdout?.truncated);
2667
+ const stderrTruncated = Boolean(result?.stderr?.truncated);
2668
+ const timedOut = Boolean(result?.timedOut);
2669
+ const aborted = Boolean(result?.aborted);
2670
+ const exitCode = typeof result?.exitCode === "number" ? result.exitCode : null;
2671
+ const sandbox = result?.sandbox;
2672
+ const sandboxIncomplete = !sandbox || sandbox.runnerFailed === true || sandbox.enforcement !== "full";
2673
+ const sandboxDenied = Boolean(sandbox?.denied);
2674
+ const validationDir = join(opts.runDir, "validation");
2675
+ await mkdir(validationDir, { recursive: true });
2676
+ const prefix = `${safe(opts.phase)}-${safe(opts.runId)}-${safe(opts.commandId)}`;
2677
+ const stdoutPath = join(validationDir, `${prefix}.stdout.log`);
2678
+ const stderrPath = join(validationDir, `${prefix}.stderr.log`);
2679
+ await Promise.all([writeFile(stdoutPath, stdout), writeFile(stderrPath, stderr)]);
2680
+ const after = await snapshotDirty(opts.cwd);
2681
+ const mutated = snapshotHash(before) !== snapshotHash(after);
2682
+ const status = sandboxDenied || mutated ? "UNSAFE_MUTATION" : sandboxIncomplete || stdoutTruncated || stderrTruncated || timedOut || aborted ? "INCONCLUSIVE" : exitCode === 0 ? "PASS" : "FAIL";
2683
+ return {
2684
+ schemaVersion: 1,
2685
+ runId: opts.runId,
2686
+ phase: opts.phase,
2687
+ commandId: opts.commandId,
2688
+ command: opts.command,
2689
+ start,
2690
+ end: (/* @__PURE__ */ new Date()).toISOString(),
2691
+ timeoutMs: opts.timeoutMs,
2692
+ exitCode: timedOut || aborted ? null : exitCode,
2693
+ status,
2694
+ stdout: {
2695
+ path: stdoutPath,
2696
+ sha256: hashBytes(stdout),
2697
+ bytes: stdout.length,
2698
+ truncated: stdoutTruncated
2699
+ },
2700
+ stderr: {
2701
+ path: stderrPath,
2702
+ sha256: hashBytes(stderr),
2703
+ bytes: stderr.length,
2704
+ truncated: stderrTruncated
2705
+ },
2706
+ boundHead: head,
2707
+ ownershipFingerprint: opts.ownershipFingerprint ?? snapshotHash(after),
2708
+ complete: !sandboxIncomplete && !sandboxDenied && !timedOut && !aborted && !stdoutTruncated && !stderrTruncated
2709
+ };
2710
+ }
2711
+ //#endregion
2712
+ //#region src/orchestration/reviewer.ts
2713
+ function parseReviewVerdict(text) {
2714
+ const first = text.split(/\r?\n/).find((l) => l.trim())?.trim();
2715
+ if ([...text.matchAll(/\[REVIEW:(PASS|FAIL)\]/g)].map((m) => m[1]).length !== 1 || !first || !/^\[REVIEW:(PASS|FAIL)\]$/.test(first)) return {
2716
+ kind: "PROTOCOL_INVALID",
2717
+ detail: "Reviewer must emit exactly one leading verdict marker."
2718
+ };
2719
+ return first === "[REVIEW:PASS]" ? {
2720
+ kind: "PASS",
2721
+ detail: text
2722
+ } : {
2723
+ kind: "FAIL",
2724
+ detail: text
2725
+ };
2726
+ }
2727
+ const REVIEW_CONTRACT = `You are an independent read-only Reviewer. Do not trust Worker/Integrator self-reports. Review the authoritative plan, actual current diff, owned-path state, trusted validation receipts, and only necessary fresh semantic evidence. Check requirement fidelity, Decision Locks, scope integrity, ownership, correctness, compatibility, regression, security/data risk, validation sufficiency, planned task completeness, unrequested changes, and the actual diff. First non-blank line must be exactly [REVIEW:PASS] or [REVIEW:FAIL]. Emit exactly one marker. On FAIL, identify only concrete Plan-required defects suitable for a targeted fix.`;
2728
+ //#endregion
2729
+ //#region src/telemetry/aggregate.ts
2730
+ function aggregateUsage(samples) {
2731
+ const sum = (k) => {
2732
+ const vals = samples.map((s) => s[k]).filter((v) => typeof v === "number");
2733
+ return vals.length && vals.length === samples.length ? vals.reduce((a, b) => a + b, 0) : void 0;
2734
+ };
2735
+ return {
2736
+ input: sum("input"),
2737
+ uncachedInput: sum("uncachedInput"),
2738
+ cacheRead: sum("cacheRead"),
2739
+ cacheWrite: sum("cacheWrite"),
2740
+ output: sum("output"),
2741
+ turns: sum("turns"),
2742
+ durationMs: sum("durationMs"),
2743
+ costUsd: void 0,
2744
+ samples
2745
+ };
2746
+ }
2747
+ //#endregion
2748
+ //#region src/orchestration/engine.ts
2749
+ const unionOwnership = (plan) => [...new Set(plan.tasks.flatMap((task) => task.modify))].sort();
2750
+ const currentRoute = (agent) => ({
2751
+ provider: agent.options?.provider,
2752
+ model: agent.options?.model,
2753
+ reasoningEffort: agent.options?.reasoningEffort,
2754
+ maxTokens: agent.options?.maxTokens
2755
+ });
2756
+ async function resolvedChoices(ctx, settings, role, agent) {
2757
+ const candidates = routeChoices(settings.roles[role], currentRoute(agent));
2758
+ const good = [];
2759
+ const failures = [];
2760
+ for (const choice of candidates) try {
2761
+ good.push(await preflightRoute(ctx.llm, choice));
2762
+ } catch (error) {
2763
+ failures.push(`${choice.provider}/${choice.model}: ${error.message}`);
2764
+ }
2765
+ if (good.length === 0) throw new Error(`no valid ${role} route${failures.length ? ` (${failures.join("; ")})` : ""}`);
2766
+ return good;
2767
+ }
2768
+ function continuationPacket(base, error) {
2769
+ return `${base}\n\nCONTINUATION AFTER PROVIDER/TRANSPORT FAILURE:\nThe previous attempt may have mutated the working tree. First inspect the actual current tree. Preserve valid existing changes, complete only remaining Plan-required work, and never reset/checkout/clean/stash or blindly replay the task. Previous failure: ${String(error?.code ?? error?.message ?? error)}`;
2770
+ }
2771
+ /**
2772
+ * Fresh fallbacks are allowed until mutation occurs. Once any run-owned mutation
2773
+ * is observed, exactly one continuation attempt on the next route is allowed.
2774
+ */
2775
+ async function runMutationRole(root, allowed, routes, run) {
2776
+ const logicalBaseline = await snapshotDirty(root);
2777
+ let lastError;
2778
+ for (let index = 0; index < routes.length; index++) try {
2779
+ return await run(routes[index], false, lastError);
2780
+ } catch (error) {
2781
+ lastError = error;
2782
+ if (!eligibleTransportFailure(error)) throw error;
2783
+ const changed = deltaPaths(logicalBaseline, await snapshotDirty(root));
2784
+ assertOwnedPaths(changed, allowed);
2785
+ if (changed.length > 0) {
2786
+ if (index + 1 >= routes.length) throw error;
2787
+ return run(routes[index + 1], true, error);
2788
+ }
2789
+ if (index + 1 >= routes.length) throw error;
2790
+ }
2791
+ throw lastError;
2792
+ }
2793
+ async function updateCheckpoint(store, launch, runId, root, head, ownership, phase, options = {}) {
2794
+ const snapshot = await snapshotDirty(root);
2795
+ const checkpoint = {
2796
+ schemaVersion: 1,
2797
+ head,
2798
+ ownership: [...ownership].sort(),
2799
+ changedPaths: Object.keys(snapshot.paths).sort(),
2800
+ fingerprint: snapshotHash(snapshot),
2801
+ phase,
2802
+ role: options.role,
2803
+ completedTaskIds: options.completedTaskIds ?? [],
2804
+ safeBoundary: options.safeBoundary ?? false,
2805
+ at: (/* @__PURE__ */ new Date()).toISOString()
2806
+ };
2807
+ await atomicJson(join(store.runDir(launch.sessionId, runId), "recovery.json"), checkpoint);
2808
+ const manifest = await store.readManifest(launch.sessionId, runId);
2809
+ manifest.phase = phase;
2810
+ manifest.repoRoot = root;
2811
+ manifest.baselineHead = head;
2812
+ manifest.ownership = [...ownership].sort();
2813
+ manifest.completedTaskIds = checkpoint.completedTaskIds;
2814
+ await store.writeManifest(manifest);
2815
+ return checkpoint;
2816
+ }
2817
+ async function writeWorktreeManifest(runDir, records) {
2818
+ await atomicJson(join(runDir, "worktrees.json"), {
2819
+ schemaVersion: 1,
2820
+ worktrees: records
2821
+ });
2822
+ }
2823
+ function dirtyOwnershipConflict(runBaseline, tasks) {
2824
+ const dirty = new Set(Object.keys(runBaseline.paths));
2825
+ return tasks.some((task) => task.modify.some((path) => dirty.has(path)));
2826
+ }
2827
+ async function boundedTextDiff(root, paths, preExisting, capBytes = 256 * 1024) {
2828
+ const cleanPaths = paths.filter((path) => !preExisting.includes(path));
2829
+ const dirtyPaths = paths.filter((path) => preExisting.includes(path));
2830
+ let text = "";
2831
+ if (cleanPaths.length > 0) {
2832
+ const raw = decodeUtf8Strict((await git(root, [
2833
+ "diff",
2834
+ "--no-ext-diff",
2835
+ "--unified=3",
2836
+ "HEAD",
2837
+ "--",
2838
+ ...cleanPaths
2839
+ ], { maxBuffer: capBytes * 2 })).stdout);
2840
+ text += raw.length > capBytes ? `${raw.slice(0, capBytes)}\n[diff truncated by host]\n` : raw;
2841
+ }
2842
+ if (dirtyPaths.length > 0) text += `\nPre-existing dirty paths changed during this run; HEAD diff is not a clean attribution for these paths. Inspect current contents and the host fingerprints instead:\n${dirtyPaths.map((path) => `- ${path}`).join("\n")}\n`;
2843
+ return text || "(no textual diff; binary/new/deleted paths may still be present)";
2844
+ }
2845
+ function reviewerPrompt(plan, changed, preExisting, receipts, actualDiff) {
2846
+ return `${REVIEW_CONTRACT}\n\nAuthoritative PlanArtifact:\n${JSON.stringify(plan, null, 2)}\n\nRun-owned changed paths:\n${changed.map((path) => `- ${path}`).join("\n") || "- none"}\n\nPre-existing dirty paths (not attributable to this run unless their fingerprint changed after the run baseline):\n${preExisting.map((path) => `- ${path}`).join("\n") || "- none"}\n\nHost-observed actual diff/evidence:\n${actualDiff}\n\nTrusted validation receipt index:\n${JSON.stringify(receipts, null, 2)}\n\nUse read-only tools only when additional current-file evidence is necessary.`;
2847
+ }
2848
+ async function nativeReviewer(ctx, parent, prompt, routes, signal, label) {
2849
+ let last;
2850
+ for (let index = 0; index < routes.length; index++) {
2851
+ const route = routes[index];
2852
+ const started = Date.now();
2853
+ try {
2854
+ const run = await ctx.subagents.start("spawn", {
2855
+ label: `${label}-${index}`,
2856
+ prompt: [{
2857
+ type: "text",
2858
+ text: prompt
2859
+ }],
2860
+ parent,
2861
+ signal,
2862
+ agentOptions: route,
2863
+ maxDepth: 1,
2864
+ toolFilter: { allow: [
2865
+ "read",
2866
+ "glob",
2867
+ "grep",
2868
+ "lsp",
2869
+ "web_search",
2870
+ "web_fetch"
2871
+ ] },
2872
+ persona: "Independent Reviewer. Strictly read-only. Review host evidence and current files; never mutate repository or external state."
2873
+ });
2874
+ try {
2875
+ const result = await run.result;
2876
+ if (result.stopReason !== "completed") {
2877
+ const error = /* @__PURE__ */ new Error(`${label} stopped: ${result.stopReason}${result.diagnostic ? `: ${result.diagnostic}` : ""}`);
2878
+ error.code = result.stopReason;
2879
+ throw error;
2880
+ }
2881
+ const text = result.output.filter((block) => block.type === "text").map((block) => block.text).join("\n");
2882
+ const usage = run.localAgent ? usageFromSession(ctx, "reviewer", run.localAgent, route) : {
2883
+ role: "reviewer",
2884
+ provider: route.provider,
2885
+ model: route.model,
2886
+ source: "unavailable"
2887
+ };
2888
+ usage.durationMs = Date.now() - started;
2889
+ return {
2890
+ text,
2891
+ usage
2892
+ };
2893
+ } finally {
2894
+ await run.dispose();
2895
+ }
2896
+ } catch (error) {
2897
+ last = error;
2898
+ if (!eligibleTransportFailure(error) || index === routes.length - 1) throw error;
2899
+ }
2900
+ }
2901
+ throw last;
2902
+ }
2903
+ async function validateIsolated(root, head, leaseId, runDir, runId, changed, plan, settings, store, phase, emit) {
2904
+ if (plan.validationCommands.length === 0) return [];
2905
+ const fingerprint = await ownedFingerprint(root, changed);
2906
+ const patch = await capturePatch(root, head, "__validation__", changed);
2907
+ const lease = await createWorktree(root, leaseId, "__validation__", head, store.root);
2908
+ let clean = false;
2909
+ try {
2910
+ await applyPatchArtifact(lease.path, patch, changed);
2911
+ const receipts = [];
2912
+ for (const command of plan.validationCommands) {
2913
+ const receipt = await runValidation({
2914
+ cwd: lease.path,
2915
+ runDir,
2916
+ runId,
2917
+ phase,
2918
+ commandId: command.id,
2919
+ command: command.command,
2920
+ timeoutMs: command.timeoutMs,
2921
+ capBytes: Math.min(settings.review.outputCapBytes, 16 * 1024 * 1024),
2922
+ ownershipFingerprint: fingerprint
2923
+ });
2924
+ receipts.push(receipt);
2925
+ await atomicJson(join(runDir, "validation", `${phase.toLowerCase()}-${command.id}.receipt.json`), receipt);
2926
+ emit?.("validation", {
2927
+ runId,
2928
+ commandId: command.id,
2929
+ status: receipt.status
2930
+ });
2931
+ if (receipt.status !== "PASS") throw new Error(`${phase.toLowerCase()} validation ${command.id}: ${receipt.status}`);
2932
+ }
2933
+ await assertTrustedReceipts(receipts, head, fingerprint);
2934
+ clean = true;
2935
+ return receipts;
2936
+ } finally {
2937
+ if (clean || !settings.execution.keepFailedWorktrees) await removeOwnedWorktree(root, lease, true).catch(() => {});
2938
+ }
2939
+ }
2940
+ async function executeSerialTask(deps, launch, task, plan, root, workerRoutes, signal, handoffs, usage) {
2941
+ deps.emit?.("task-start", {
2942
+ runId: launch.__runId,
2943
+ taskId: task.id
2944
+ });
2945
+ const before = await snapshotDirty(root);
2946
+ const basePacket = buildTaskPacket(plan, task, handoffs);
2947
+ const backend = new NativeSpawnBackend(deps.ctx);
2948
+ const result = await runMutationRole(root, task.modify, workerRoutes, (route, continuation, error) => backend.run({
2949
+ parent: launch.agent,
2950
+ role: "worker",
2951
+ taskId: task.id,
2952
+ prompt: continuation ? continuationPacket(basePacket, error) : basePacket,
2953
+ route,
2954
+ signal,
2955
+ persona: "Fresh Worker. Implement only the assigned Plan task and exact modify ownership. Never commit, push, reset, clean, stash, or discard user work.",
2956
+ ownership: {
2957
+ root,
2958
+ paths: task.modify
2959
+ }
2960
+ }));
2961
+ if (result.status !== "COMPLETE") throw new Error(`worker ${task.id}: ${result.status}`);
2962
+ if (result.__usage) usage.push(result.__usage);
2963
+ const changed = deltaPaths(before, await snapshotDirty(root));
2964
+ assertOwnedPaths(changed, task.modify);
2965
+ return {
2966
+ task,
2967
+ result,
2968
+ changed
2969
+ };
2970
+ }
2971
+ async function executeWorktreeTask(deps, launch, runId, leaseRunId, runDir, root, head, task, plan, workerRoutes, signal, handoffs, usage, records, seedPatch) {
2972
+ deps.emit?.("task-start", {
2973
+ runId,
2974
+ taskId: task.id
2975
+ });
2976
+ const lease = await createWorktree(root, leaseRunId, task.id, head, deps.store.root);
2977
+ const record = {
2978
+ taskId: task.id,
2979
+ path: lease.path,
2980
+ baseHead: head,
2981
+ status: "ACTIVE"
2982
+ };
2983
+ records.push(record);
2984
+ await writeWorktreeManifest(runDir, records);
2985
+ let accepted = false;
2986
+ try {
2987
+ if (seedPatch) await applyPatchArtifact(lease.path, seedPatch, seedPatch.files.map((file) => file.path));
2988
+ const before = await snapshotDirty(lease.path);
2989
+ const basePacket = `${buildTaskPacket(plan, task, handoffs)}\n\nIMPORTANT: return exactly one JSON object matching the completion contract, with no markdown fence or prose.`;
2990
+ const backend = new SdkWorkspaceBackend();
2991
+ const result = await runMutationRole(lease.path, task.modify, workerRoutes, (route, continuation, error) => backend.run({
2992
+ cwd: lease.path,
2993
+ profile: deps.settings(root).execution.sdkProfile,
2994
+ taskId: task.id,
2995
+ prompt: continuation ? continuationPacket(basePacket, error) : basePacket,
2996
+ route,
2997
+ signal
2998
+ }));
2999
+ if (result.status !== "COMPLETE") throw new Error(`worker ${task.id}: ${result.status}`);
3000
+ if (result.__usage) usage.push(result.__usage);
3001
+ const after = await snapshotDirty(lease.path);
3002
+ const changed = deltaPaths(before, after);
3003
+ assertOwnedPaths(changed, task.modify);
3004
+ const patch = await capturePatch(lease.path, head, task.id, changed);
3005
+ await atomicJson(join(runDir, "patches", `${task.id}.json`), patch);
3006
+ record.status = "CAPTURED";
3007
+ record.fingerprint = snapshotHash(after);
3008
+ record.patchSha256 = patch.sha256;
3009
+ await writeWorktreeManifest(runDir, records);
3010
+ accepted = true;
3011
+ return {
3012
+ task,
3013
+ result,
3014
+ changed,
3015
+ patch
3016
+ };
3017
+ } catch (error) {
3018
+ record.status = "FAILED";
3019
+ await writeWorktreeManifest(runDir, records).catch(() => {});
3020
+ throw error;
3021
+ } finally {
3022
+ if (accepted || !deps.settings(root).execution.keepFailedWorktrees) {
3023
+ await removeOwnedWorktree(root, lease, true).catch(() => {});
3024
+ record.status = "CLEANED";
3025
+ await writeWorktreeManifest(runDir, records).catch(() => {});
3026
+ }
3027
+ }
3028
+ }
3029
+ function normalizeWaveForResume(wave, completed) {
3030
+ const taskIds = wave.taskIds.filter((id) => !completed.has(id));
3031
+ if (taskIds.length === 0) return void 0;
3032
+ return taskIds.length === 1 ? {
3033
+ mode: "serial",
3034
+ taskIds
3035
+ } : {
3036
+ ...wave,
3037
+ taskIds
3038
+ };
3039
+ }
3040
+ function createOrchestratorRunner(deps) {
3041
+ return async (launch, runId, signal) => {
3042
+ launch.__runId = runId;
3043
+ const { ctx, store } = deps;
3044
+ const plan = launch.artifact;
3045
+ const root = await repoRoot(launch.agent.session.header?.cwd ?? process.cwd());
3046
+ const settings = deps.settings(root);
3047
+ const head = await fullHead(root);
3048
+ if (launch.baselineHead && launch.baselineHead !== head) throw new Error(`approved baseline HEAD drift: ${launch.baselineHead} -> ${head}`);
3049
+ const allOwned = unionOwnership(plan);
3050
+ await assertRepoPathsConfined(root, allOwned);
3051
+ const runDir = store.runDir(launch.sessionId, runId);
3052
+ await mkdir(join(runDir, "patches"), { recursive: true });
3053
+ const baselinePath = join(runDir, "baseline.json");
3054
+ let runBaseline;
3055
+ if (launch.resumeFrom) runBaseline = await readJson(baselinePath);
3056
+ else {
3057
+ runBaseline = await snapshotDirty(root);
3058
+ await atomicJson(baselinePath, runBaseline);
3059
+ await updateCheckpoint(store, launch, runId, root, head, allOwned, "PREFLIGHT", {
3060
+ safeBoundary: true,
3061
+ completedTaskIds: []
3062
+ });
3063
+ }
3064
+ const workerRoutes = await resolvedChoices(ctx, settings, "worker", launch.agent);
3065
+ const handoffPath = join(runDir, "handoffs.json");
3066
+ const handoffs = launch.resumeFrom ? await readJson(handoffPath).catch(() => []) : [];
3067
+ const usage = [];
3068
+ const worktreeRecords = await readJson(join(runDir, "worktrees.json")).then((v) => v.worktrees).catch(() => []);
3069
+ const completed = new Set(launch.completedTaskIds ?? []);
3070
+ let hadWorktree = worktreeRecords.some((record) => record.status === "CAPTURED" || record.status === "CLEANED");
3071
+ if (launch.resumeFrom !== "VALIDATING") {
3072
+ deps.emit?.("phase", {
3073
+ runId,
3074
+ sessionId: launch.sessionId,
3075
+ phase: "WORKERS"
3076
+ });
3077
+ const configuredWaves = buildWaves(plan, settings.execution.maxParallelWorkers, settings.execution.parallelMode);
3078
+ const leaseRunId = `${runId}-worktrees`;
3079
+ for (const configured of configuredWaves) {
3080
+ const wave = normalizeWaveForResume(configured, completed);
3081
+ if (!wave) continue;
3082
+ signal.throwIfAborted();
3083
+ const waveTasks = wave.taskIds.map((id) => plan.tasks.find((task) => task.id === id));
3084
+ if (!(wave.mode === "parallel" && recheckWave(plan, wave) && !dirtyOwnershipConflict(runBaseline, waveTasks))) {
3085
+ for (const task of waveTasks) {
3086
+ const outcome = await executeSerialTask(deps, launch, task, plan, root, workerRoutes, signal, handoffs, usage);
3087
+ handoffs.push(`${task.id}: host-observed=${outcome.changed.join(",") || "(none)"} status=${outcome.result.status}`);
3088
+ completed.add(task.id);
3089
+ deps.emit?.("task-end", {
3090
+ runId,
3091
+ taskId: task.id,
3092
+ status: outcome.result.status
3093
+ });
3094
+ await atomicJson(handoffPath, handoffs);
3095
+ await updateCheckpoint(store, launch, runId, root, head, allOwned, "WORKERS", {
3096
+ completedTaskIds: [...completed],
3097
+ safeBoundary: true
3098
+ });
3099
+ }
3100
+ continue;
3101
+ }
3102
+ hadWorktree = true;
3103
+ const mainBefore = await snapshotDirty(root);
3104
+ if (mainBefore.head !== head) throw new Error("HEAD drift before parallel wave");
3105
+ const priorRunDelta = deltaPaths(runBaseline, mainBefore);
3106
+ assertOwnedPaths(priorRunDelta, allOwned);
3107
+ const seedPatch = priorRunDelta.length > 0 ? await capturePatch(root, head, "__wave_seed__", priorRunDelta) : void 0;
3108
+ const outcomes = await Promise.all(waveTasks.map((task) => executeWorktreeTask(deps, launch, runId, leaseRunId, runDir, root, head, task, plan, workerRoutes, signal, handoffs, usage, worktreeRecords, seedPatch)));
3109
+ if (snapshotHash(await snapshotDirty(root)) !== snapshotHash(mainBefore)) throw new Error("working tree drift while parallel workers were isolated; refusing to apply patches");
3110
+ if (await fullHead(root) !== head) throw new Error("HEAD drift before applying parallel worker patches");
3111
+ for (const outcome of outcomes) {
3112
+ if (!outcome.patch) throw new Error(`missing patch artifact for ${outcome.task.id}`);
3113
+ await applyPatchArtifact(root, outcome.patch, allOwned);
3114
+ handoffs.push(`${outcome.task.id}: host-observed=${outcome.changed.join(",") || "(none)"} status=${outcome.result.status}`);
3115
+ completed.add(outcome.task.id);
3116
+ deps.emit?.("task-end", {
3117
+ runId,
3118
+ taskId: outcome.task.id,
3119
+ status: outcome.result.status
3120
+ });
3121
+ }
3122
+ await atomicJson(handoffPath, handoffs);
3123
+ await updateCheckpoint(store, launch, runId, root, head, allOwned, "WORKERS", {
3124
+ completedTaskIds: [...completed],
3125
+ safeBoundary: true
3126
+ });
3127
+ }
3128
+ if (completed.size !== plan.tasks.length) throw new Error(`worker stage incomplete: ${completed.size}/${plan.tasks.length}`);
3129
+ if (needsIntegrator(plan.tasks.length, hadWorktree, plan.tasks.length > 1, false)) {
3130
+ deps.emit?.("phase", {
3131
+ runId,
3132
+ sessionId: launch.sessionId,
3133
+ phase: "INTEGRATING"
3134
+ });
3135
+ const integratorBefore = await snapshotDirty(root);
3136
+ const routes = await resolvedChoices(ctx, settings, "integrator", launch.agent);
3137
+ const backend = new NativeSpawnBackend(ctx);
3138
+ const basePrompt = integratorPrompt(JSON.stringify(plan), handoffs, hadWorktree ? [join(runDir, "patches")] : []);
3139
+ const result = await runMutationRole(root, allOwned, routes, (route, continuation, error) => backend.run({
3140
+ parent: launch.agent,
3141
+ role: "integrator",
3142
+ taskId: "__integrator__",
3143
+ prompt: continuation ? continuationPacket(basePrompt, error) : basePrompt,
3144
+ route,
3145
+ signal,
3146
+ persona: "Integrator. Reconcile only Plan-required cross-task wiring. Never redesign, commit, push, reset, clean, stash, or touch files outside union ownership.",
3147
+ ownership: {
3148
+ root,
3149
+ paths: allOwned
3150
+ }
3151
+ }));
3152
+ if (result.status !== "COMPLETE") throw new Error(`integrator: ${result.status}`);
3153
+ if (result.__usage) usage.push(result.__usage);
3154
+ assertOwnedPaths(deltaPaths(integratorBefore, await snapshotDirty(root)), allOwned);
3155
+ }
3156
+ const afterMutation = await snapshotDirty(root);
3157
+ assertOwnedPaths(deltaPaths(runBaseline, afterMutation), allOwned);
3158
+ await updateCheckpoint(store, launch, runId, root, head, allOwned, "VALIDATING", {
3159
+ completedTaskIds: [...completed],
3160
+ safeBoundary: true
3161
+ });
3162
+ }
3163
+ let finalSnapshot = await snapshotDirty(root);
3164
+ let runDelta = deltaPaths(runBaseline, finalSnapshot);
3165
+ assertOwnedPaths(runDelta, allOwned);
3166
+ deps.emit?.("phase", {
3167
+ runId,
3168
+ sessionId: launch.sessionId,
3169
+ phase: "VALIDATING"
3170
+ });
3171
+ let receipts = await validateIsolated(root, head, `${runId}-validation-${Date.now()}`, runDir, runId, runDelta, plan, settings, store, "VALIDATING", deps.emit);
3172
+ await updateCheckpoint(store, launch, runId, root, head, allOwned, "REVIEWING", {
3173
+ completedTaskIds: [...completed],
3174
+ safeBoundary: true
3175
+ });
3176
+ deps.emit?.("phase", {
3177
+ runId,
3178
+ sessionId: launch.sessionId,
3179
+ phase: "REVIEWING"
3180
+ });
3181
+ const reviewRoutes = await resolvedChoices(ctx, settings, "reviewer", launch.agent);
3182
+ const preExisting = Object.keys(runBaseline.paths).sort();
3183
+ let fixRound = 0;
3184
+ while (true) {
3185
+ finalSnapshot = await snapshotDirty(root);
3186
+ runDelta = deltaPaths(runBaseline, finalSnapshot);
3187
+ assertOwnedPaths(runDelta, allOwned);
3188
+ const fingerprint = await ownedFingerprint(root, runDelta);
3189
+ await assertTrustedReceipts(receipts, head, fingerprint);
3190
+ const actualDiff = await boundedTextDiff(root, runDelta, preExisting);
3191
+ const prompt = reviewerPrompt(plan, runDelta, preExisting, receiptIndex(receipts), actualDiff);
3192
+ let reviewerText = "";
3193
+ let verdict = {
3194
+ kind: "PROTOCOL_INVALID",
3195
+ detail: ""
3196
+ };
3197
+ for (let protocolAttempt = 0; protocolAttempt <= settings.review.protocolRetry; protocolAttempt++) {
3198
+ const reviewed = await nativeReviewer(ctx, launch.agent, prompt, reviewRoutes, signal, `review-${fixRound}-${protocolAttempt}`);
3199
+ reviewerText = reviewed.text;
3200
+ usage.push(reviewed.usage);
3201
+ verdict = parseReviewVerdict(reviewerText);
3202
+ if (verdict.kind !== "PROTOCOL_INVALID") break;
3203
+ }
3204
+ if (verdict.kind === "PROTOCOL_INVALID") throw new Error("Reviewer protocol invalid after retry");
3205
+ deps.emit?.("review", {
3206
+ runId,
3207
+ round: fixRound,
3208
+ verdict: verdict.kind
3209
+ });
3210
+ await atomicJson(join(runDir, `review-${fixRound}.json`), {
3211
+ verdict,
3212
+ reviewerText
3213
+ });
3214
+ if (verdict.kind === "PASS") break;
3215
+ if (fixRound >= settings.review.maxReviewRounds) throw new Error("Reviewer failed after maxReviewRounds");
3216
+ fixRound += 1;
3217
+ deps.emit?.("phase", {
3218
+ runId,
3219
+ sessionId: launch.sessionId,
3220
+ phase: "FIXING",
3221
+ reviewRound: fixRound
3222
+ });
3223
+ const fixBefore = await snapshotDirty(root);
3224
+ const backend = new NativeSpawnBackend(ctx);
3225
+ const basePrompt = `Targeted fix only. Reviewer findings:\n${verdict.detail}\n\nAuthoritative PlanArtifact:\n${JSON.stringify(plan)}\n\nYou may modify only these paths:\n${allOwned.join("\n")}\nDo not redesign, expand scope, commit, push, reset, clean, stash, or discard user work.`;
3226
+ const result = await runMutationRole(root, allOwned, workerRoutes, (route, continuation, error) => backend.run({
3227
+ parent: launch.agent,
3228
+ role: "worker",
3229
+ taskId: `__fix_${fixRound}__`,
3230
+ prompt: continuation ? continuationPacket(basePrompt, error) : basePrompt,
3231
+ route,
3232
+ signal,
3233
+ persona: "Targeted fix Worker. Fix only Reviewer-identified Plan-required defects.",
3234
+ ownership: {
3235
+ root,
3236
+ paths: allOwned
3237
+ }
3238
+ }));
3239
+ if (result.status !== "COMPLETE") throw new Error(`targeted fix: ${result.status}`);
3240
+ if (result.__usage) usage.push(result.__usage);
3241
+ assertOwnedPaths(deltaPaths(fixBefore, await snapshotDirty(root)), allOwned);
3242
+ finalSnapshot = await snapshotDirty(root);
3243
+ runDelta = deltaPaths(runBaseline, finalSnapshot);
3244
+ assertOwnedPaths(runDelta, allOwned);
3245
+ deps.emit?.("phase", {
3246
+ runId,
3247
+ sessionId: launch.sessionId,
3248
+ phase: "VALIDATING",
3249
+ reviewRound: fixRound
3250
+ });
3251
+ receipts = await validateIsolated(root, head, `${runId}-fix-${fixRound}-${Date.now()}`, runDir, runId, runDelta, plan, settings, store, "FIXING", deps.emit);
3252
+ await updateCheckpoint(store, launch, runId, root, head, allOwned, "REVIEWING", {
3253
+ completedTaskIds: [...completed],
3254
+ safeBoundary: true
3255
+ });
3256
+ }
3257
+ finalSnapshot = await snapshotDirty(root);
3258
+ const finalDelta = deltaPaths(runBaseline, finalSnapshot);
3259
+ assertOwnedPaths(finalDelta, allOwned);
3260
+ const finalFingerprint = await ownedFingerprint(root, finalDelta);
3261
+ await assertTrustedReceipts(receipts, head, finalFingerprint);
3262
+ const usageSummary = aggregateUsage(usage);
3263
+ await atomicJson(join(runDir, "telemetry.json"), usageSummary);
3264
+ if (launch.externalIssue?.publishAfterPass) {
3265
+ const { publishExternalRun } = await import("./publication-D352itYY.js");
3266
+ await publishExternalRun({
3267
+ cwd: root,
3268
+ runDir,
3269
+ meta: launch.externalIssue,
3270
+ ownedPaths: finalDelta,
3271
+ expectedHead: head,
3272
+ expectedOwnershipFingerprint: finalFingerprint,
3273
+ receipts
3274
+ });
3275
+ }
3276
+ deps.emit?.("phase", {
3277
+ runId,
3278
+ sessionId: launch.sessionId,
3279
+ phase: "COMPLETE"
3280
+ });
3281
+ await atomicJson(join(runDir, "completion.json"), {
3282
+ runId,
3283
+ planHash: launch.planHash,
3284
+ review: "PASS",
3285
+ changedPaths: finalDelta,
3286
+ ownershipFingerprint: finalFingerprint,
3287
+ receipts: receiptIndex(receipts),
3288
+ usage: usageSummary,
3289
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3290
+ });
3291
+ try {
3292
+ launch.agent.inject(createUserMessage({
3293
+ content: [{
3294
+ type: "text",
3295
+ text: `Plan Orchestrator completed run ${runId}. Reviewer: PASS. Changed paths: ${finalDelta.join(", ") || "(none)"}. Validation: ${receipts.map((receipt) => `${receipt.commandId}=${receipt.status}`).join(", ") || "no host commands"}.`
3296
+ }],
3297
+ source: {
3298
+ kind: "plugin",
3299
+ plugin: "plan-orchestrator"
3300
+ }
3301
+ }));
3302
+ } catch {}
3303
+ };
3304
+ }
3305
+ //#endregion
3306
+ //#region src/orchestration/projection.ts
3307
+ const phaseSchema = z.enum([
3308
+ "IDLE",
3309
+ "PLANNING",
3310
+ "PLAN_VALIDATED",
3311
+ "APPROVED_PENDING",
3312
+ "PREFLIGHT",
3313
+ "WORKERS",
3314
+ "INTEGRATING",
3315
+ "VALIDATING",
3316
+ "REVIEWING",
3317
+ "FIXING",
3318
+ "COMPLETE",
3319
+ "BLOCKED",
3320
+ "FAILED",
3321
+ "INTERRUPTED",
3322
+ "CANCELLED"
3323
+ ]);
3324
+ const runSchema = z.object({
3325
+ runId: z.string().min(1).max(200),
3326
+ sessionId: z.string().min(1).max(300),
3327
+ phase: phaseSchema,
3328
+ status: z.string().max(100),
3329
+ tasksTotal: z.number().int().nonnegative(),
3330
+ tasksDone: z.number().int().nonnegative(),
3331
+ activeTaskIds: z.array(z.string().max(200)).max(64),
3332
+ reviewRound: z.number().int().nonnegative(),
3333
+ message: z.string().max(2e3).optional(),
3334
+ startedAt: z.string().max(64),
3335
+ updatedAt: z.string().max(64)
3336
+ }).strict();
3337
+ const stateSchema = z.object({ runs: z.array(runSchema).max(20) }).strict();
3338
+ function replaceRun(state, run) {
3339
+ return { runs: [run, ...state.runs.filter((row) => row.runId !== run.runId)].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 20) };
3340
+ }
3341
+ function find(state, runId) {
3342
+ return state.runs.find((run) => run.runId === runId);
3343
+ }
3344
+ const planOrchestratorProjectionDefinition = {
3345
+ key: "planOrchestrator",
3346
+ stateVersion: 1,
3347
+ stateSchema,
3348
+ init: () => ({ runs: [] }),
3349
+ apply: (state, event) => {
3350
+ const d = event.data;
3351
+ if (event.type === "planx/run-approved") return replaceRun(state, {
3352
+ runId: d.runId,
3353
+ sessionId: d.sessionId,
3354
+ phase: "APPROVED_PENDING",
3355
+ status: "APPROVED_PENDING",
3356
+ tasksTotal: d.tasksTotal,
3357
+ tasksDone: 0,
3358
+ activeTaskIds: [],
3359
+ reviewRound: 0,
3360
+ startedAt: d.at,
3361
+ updatedAt: d.at
3362
+ });
3363
+ if (typeof d?.runId !== "string") return state;
3364
+ const current = find(state, d.runId);
3365
+ if (!current) return state;
3366
+ let next = {
3367
+ ...current,
3368
+ activeTaskIds: [...current.activeTaskIds]
3369
+ };
3370
+ switch (event.type) {
3371
+ case "planx/run-phase":
3372
+ next.phase = d.phase;
3373
+ next.status = d.phase;
3374
+ if (typeof d.reviewRound === "number") next.reviewRound = d.reviewRound;
3375
+ if (typeof d.message === "string") next.message = d.message;
3376
+ break;
3377
+ case "planx/task-start":
3378
+ if (!next.activeTaskIds.includes(d.taskId)) next.activeTaskIds.push(d.taskId);
3379
+ break;
3380
+ case "planx/task-end":
3381
+ next.activeTaskIds = next.activeTaskIds.filter((id) => id !== d.taskId);
3382
+ next.tasksDone = Math.min(next.tasksTotal, next.tasksDone + 1);
3383
+ break;
3384
+ case "planx/review":
3385
+ next.reviewRound = d.round;
3386
+ break;
3387
+ case "planx/recovery":
3388
+ next.status = d.status;
3389
+ if (typeof d.message === "string") next.message = d.message;
3390
+ break;
3391
+ case "planx/run-terminal":
3392
+ next.phase = d.phase;
3393
+ next.status = d.phase;
3394
+ next.activeTaskIds = [];
3395
+ if (typeof d.message === "string") next.message = d.message;
3396
+ break;
3397
+ default: return state;
3398
+ }
3399
+ next.updatedAt = d.at;
3400
+ return replaceRun(state, next);
3401
+ },
3402
+ wire: {
3403
+ viewSchema: stateSchema,
3404
+ view: (state) => state
3405
+ }
3406
+ };
3407
+ //#endregion
3408
+ //#region src/external/contract.ts
3409
+ function meta(value) {
3410
+ const e = value?.externalPlan;
3411
+ if (!e || e.version !== 1 || !Number.isSafeInteger(e.revision) || e.revision < 1 || typeof e.repository !== "string" || !/^[-A-Za-z0-9_.]+\/[-A-Za-z0-9_.]+$/.test(e.repository) || typeof e.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(e.baseCommit)) throw new Error("invalid externalPlan metadata");
3412
+ return {
3413
+ version: 1,
3414
+ revision: e.revision,
3415
+ repository: e.repository,
3416
+ baseCommit: e.baseCommit
3417
+ };
3418
+ }
3419
+ function validateExternalContract(value) {
3420
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("external contract object required");
3421
+ const externalPlan = meta(value);
3422
+ let planInput;
3423
+ if (value.plan && typeof value.plan === "object") planInput = value.plan;
3424
+ else {
3425
+ const { externalPlan: _ignored, ...rest } = value;
3426
+ planInput = rest;
3427
+ }
3428
+ return {
3429
+ externalPlan,
3430
+ plan: validatePlanArtifact(planInput, true)
3431
+ };
3432
+ }
3433
+ function canonical(value) {
3434
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
3435
+ if (value && typeof value === "object") return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}`;
3436
+ return JSON.stringify(value);
3437
+ }
3438
+ function selectTrustedRevision(contracts) {
3439
+ if (!contracts.length) throw new Error("no valid external contract");
3440
+ const max = Math.max(...contracts.map((c) => c.externalPlan.revision)), same = contracts.filter((c) => c.externalPlan.revision === max);
3441
+ if (new Set(same.map((c) => canonical(c))).size !== 1) throw new Error(`conflicting contracts at revision ${max}`);
3442
+ return same[0];
3443
+ }
3444
+ function extractExternalContracts(text) {
3445
+ const out = [];
3446
+ for (const m of text.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)) try {
3447
+ out.push(validateExternalContract(JSON.parse(m[1])));
3448
+ } catch {}
3449
+ return out;
3450
+ }
3451
+ function validateCompletionEvidence(value) {
3452
+ const e = value?.externalCompletion;
3453
+ if (!e || e.version !== 1 || !Number.isSafeInteger(e.revision) || e.revision < 1 || typeof e.repository !== "string" || !Number.isSafeInteger(e.issue) || e.issue < 1 || !Number.isSafeInteger(e.pr) || e.pr < 1 || typeof e.head !== "string" || !/^[0-9a-f]{40}$/.test(e.head)) throw new Error("invalid external completion evidence");
3454
+ return { externalCompletion: {
3455
+ version: 1,
3456
+ revision: e.revision,
3457
+ repository: e.repository,
3458
+ issue: e.issue,
3459
+ pr: e.pr,
3460
+ head: e.head
3461
+ } };
3462
+ }
3463
+ function extractCompletionEvidence(text) {
3464
+ if (!text.includes("IMPLEMENTATION COMPLETE")) return [];
3465
+ const out = [];
3466
+ for (const m of text.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)) try {
3467
+ out.push(validateCompletionEvidence(JSON.parse(m[1])));
3468
+ } catch {}
3469
+ return out;
3470
+ }
3471
+ //#endregion
3472
+ //#region src/external/continuation.ts
3473
+ function completionIsCurrent(evidence, revision, remoteHead) {
3474
+ return Boolean(evidence && evidence.revision === revision && evidence.head === remoteHead);
3475
+ }
3476
+ async function findCurrentCompletion(evidences, opts) {
3477
+ const matching = evidences.map((x) => x.externalCompletion).filter((x) => x.revision === opts.revision && x.repository === opts.repository && x.issue === opts.issue).sort((a, b) => b.pr - a.pr);
3478
+ for (const e of matching) {
3479
+ const head = await opts.remoteHead(e.pr);
3480
+ if (head && completionIsCurrent({
3481
+ revision: e.revision,
3482
+ head: e.head
3483
+ }, opts.revision, head)) return e;
3484
+ }
3485
+ }
3486
+ //#endregion
3487
+ //#region src/external/issue-command.ts
3488
+ function parseIssueInput(raw) {
3489
+ const m = /^\s*(\d+)\s*$/.exec(raw);
3490
+ if (!m) return void 0;
3491
+ const n = Number(m[1]);
3492
+ return Number.isSafeInteger(n) && n > 0 ? n : void 0;
3493
+ }
3494
+ const issueBranch = (n) => `issue-${n}-external-plan`;
3495
+ function hashPlan(plan) {
3496
+ return createHash("sha256").update(JSON.stringify(plan)).digest("hex");
3497
+ }
3498
+ function installIssueCommand(ctx, deps) {
3499
+ if (!ctx.commands?.register) throw new Error("commands service unavailable");
3500
+ return ctx.commands.register({
3501
+ name: "plan-issue",
3502
+ description: "Execute a trusted external PlanArtifact from a GitHub Issue",
3503
+ input: {
3504
+ hint: "<issue-number>",
3505
+ attachments: false
3506
+ },
3507
+ recordInput: false,
3508
+ handler: async ({ agent, rawInput }) => {
3509
+ try {
3510
+ const issueNumber = parseIssueInput(rawInput);
3511
+ if (!issueNumber) return {
3512
+ kind: "error",
3513
+ text: "Usage: /plan-issue <positive issue number>"
3514
+ };
3515
+ const cwd = agent.session.header?.cwd ?? process.cwd();
3516
+ const settings = deps.settings(cwd);
3517
+ if (!settings.enabled) return {
3518
+ kind: "error",
3519
+ text: "Plan Orchestrator is disabled in Settings → Plan Mode."
3520
+ };
3521
+ if (!settings.externalIssue.enabled) return {
3522
+ kind: "error",
3523
+ text: "External Issue Mode is disabled in Settings → Plan Mode."
3524
+ };
3525
+ const actual = await ghRepository(cwd);
3526
+ const repository = String(actual?.nameWithOwner ?? "");
3527
+ if (!repository) throw new Error("current GitHub repository identity unavailable");
3528
+ const texts = await trustedIssueTexts(cwd, repository, await fetchIssue(cwd, issueNumber));
3529
+ if (texts.length === 0) throw new Error("no issue body/comment from a repository writer, maintainer, or admin");
3530
+ const selected = selectTrustedRevision(texts.flatMap(extractExternalContracts).filter((contract) => contract.externalPlan.repository === repository));
3531
+ await ghPreflight(cwd, selected.externalPlan.repository);
3532
+ const current = await findCurrentCompletion(texts.flatMap(extractCompletionEvidence), {
3533
+ revision: selected.externalPlan.revision,
3534
+ repository: selected.externalPlan.repository,
3535
+ issue: issueNumber,
3536
+ remoteHead: async (pr) => {
3537
+ try {
3538
+ const remote = await remotePr(cwd, pr);
3539
+ return remote?.state === "OPEN" ? remote.headRefOid : void 0;
3540
+ } catch {
3541
+ return;
3542
+ }
3543
+ }
3544
+ });
3545
+ if (current) return {
3546
+ kind: "success",
3547
+ text: `Issue #${issueNumber} revision ${current.revision} is already complete at PR #${current.pr} (${current.head}).`
3548
+ };
3549
+ const branch = issueBranch(issueNumber);
3550
+ await prepareIssueBranch(cwd, branch, selected.externalPlan.baseCommit);
3551
+ const run = deps.orchestration.approve({
3552
+ sessionId: String(agent.session.id),
3553
+ agent,
3554
+ artifact: selected.plan,
3555
+ planHash: hashPlan(selected.plan),
3556
+ baselineHead: selected.externalPlan.baseCommit,
3557
+ externalIssue: {
3558
+ issueNumber,
3559
+ repository: selected.externalPlan.repository,
3560
+ revision: selected.externalPlan.revision,
3561
+ branch,
3562
+ publishAfterPass: Boolean(settings.externalIssue.publishAfterPass)
3563
+ }
3564
+ });
3565
+ if (!run) return {
3566
+ kind: "error",
3567
+ text: "Another Plan Orchestrator run is already active for this session."
3568
+ };
3569
+ return {
3570
+ kind: "success",
3571
+ text: `Accepted Issue #${issueNumber} external plan revision ${selected.externalPlan.revision} on ${branch}. Execution run ${run} will start automatically.${settings.externalIssue.publishAfterPass ? " PASS will publish/reuse a PR." : " Automatic PR publication is disabled."}`
3572
+ };
3573
+ } catch (e) {
3574
+ return {
3575
+ kind: "error",
3576
+ text: `External Issue Mode blocked: ${e.message}`
3577
+ };
3578
+ }
3579
+ }
3580
+ });
3581
+ }
3582
+ //#endregion
3583
+ //#region src/index.ts
3584
+ const name = "plan-orchestrator";
3585
+ const inject = [
3586
+ "settings",
3587
+ "tools",
3588
+ "llm",
3589
+ "sessions",
3590
+ "subagents",
3591
+ "systemPrompt",
3592
+ "sandboxPolicy",
3593
+ "sessionProjections",
3594
+ "shell"
3595
+ ];
3596
+ const execFileP = promisify(execFile);
3597
+ const require = createRequire(import.meta.url);
3598
+ function packageVersion(id) {
3599
+ try {
3600
+ return require(`${id}/package.json`).version;
3601
+ } catch {
3602
+ return;
3603
+ }
3604
+ }
3605
+ async function commandAvailable(command, args) {
3606
+ try {
3607
+ await execFileP(command, args, {
3608
+ windowsHide: true,
3609
+ timeout: 5e3
3610
+ });
3611
+ return true;
3612
+ } catch {
3613
+ return false;
3614
+ }
3615
+ }
3616
+ function hasService(ctx, name) {
3617
+ try {
3618
+ return ctx.get?.(name) !== void 0;
3619
+ } catch {
3620
+ return false;
3621
+ }
3622
+ }
3623
+ function apply(ctx) {
3624
+ const c = ctx;
3625
+ const settings = registerSettings(c);
3626
+ const bridge = new NativePlanBridge();
3627
+ const readOnly = new PlannerReadOnlyGuard();
3628
+ const store = new RunStore();
3629
+ c.sessionProjections.register(planOrchestratorProjectionDefinition);
3630
+ let orchestration;
3631
+ orchestration = new OrchestrationService(store, createOrchestratorRunner({
3632
+ ctx: c,
3633
+ settings: (cwd) => settings.effective(cwd),
3634
+ store,
3635
+ emit: (kind, data) => orchestration?.recordEvent(kind, data)
3636
+ }));
3637
+ const isPlanActive = (agent) => {
3638
+ try {
3639
+ return Boolean(c.sessionProjections.stateOf(agent.session, "plan")?.active);
3640
+ } catch {
3641
+ return false;
3642
+ }
3643
+ };
3644
+ const isEnabled = (agent) => Boolean(agent && settings.effective(agent.session?.header?.cwd).enabled);
3645
+ const firstPolicySeen = /* @__PURE__ */ new WeakSet();
3646
+ c.effect(() => configureValidationShell(c.shell), "plan-orchestrator: sandbox validation runtime");
3647
+ c.effect(() => configureRoleTimeoutResolver((cwd) => settings.effective(cwd).execution.roleTimeoutMs), "plan-orchestrator: role timeout runtime");
3648
+ c.systemPrompt.section({
3649
+ name: "plan-orchestrator:policy",
3650
+ order: (c.systemPrompt.getSectionOrder?.("PLAN_POLICY") ?? 500) + 1,
3651
+ text: ({ agent }) => {
3652
+ if (!agent) return "";
3653
+ const effective = settings.effective(agent.session.header?.cwd);
3654
+ const session = agent.session;
3655
+ if (!effective.enabled) {
3656
+ firstPolicySeen.delete(session);
3657
+ return "";
3658
+ }
3659
+ const active = isPlanActive(agent);
3660
+ const first = !firstPolicySeen.has(session);
3661
+ const text = plannerPolicyText(true, active, first, effective.planning);
3662
+ if (text && first) firstPolicySeen.add(session);
3663
+ return text;
3664
+ }
3665
+ });
3666
+ c.effect(() => installExitPlanValidator(c, bridge, () => settings.get().execution.requireExplicitOwnership, isEnabled), "plan-orchestrator: validate native plan");
3667
+ c.effect(() => readOnly.install(c, isEnabled), "plan-orchestrator: strict read-only tool guard");
3668
+ c.effect(() => installPlannerRoute(c, (agent) => settings.effective(agent.session.header?.cwd).roles.planner, isPlanActive, isEnabled), "plan-orchestrator: planner route");
3669
+ c.effect(() => installEnabledParentFence(c, orchestration, isEnabled), "plan-orchestrator: parent fence");
3670
+ c.on("agent/pre-step", async ({ agent }, next) => {
3671
+ const decision = await next();
3672
+ const effective = settings.effective(agent.session.header?.cwd);
3673
+ if (!effective.enabled) {
3674
+ bridge.clearSession(String(agent.session.id));
3675
+ readOnly.deactivate(agent.session, c.sandboxPolicy);
3676
+ return decision;
3677
+ }
3678
+ if (decision.kind !== "reject" && effective.planning.strictReadOnly && isPlanActive(agent)) readOnly.activate(agent.session, c.sandboxPolicy);
3679
+ else if (!isPlanActive(agent)) readOnly.deactivate(agent.session, c.sandboxPolicy);
3680
+ return decision;
3681
+ }, { prepend: true });
3682
+ c.on("tools/result", (exec, result) => {
3683
+ const staged = consumeApprovedPlanResult(bridge, exec, result, isEnabled);
3684
+ if (!staged) return;
3685
+ const sessionId = String(exec.agent?.session?.id ?? "");
3686
+ orchestration.approve({
3687
+ sessionId,
3688
+ agent: exec.agent,
3689
+ artifact: staged.artifact,
3690
+ planHash: staged.hash,
3691
+ baselineHead: staged.baselineHead
3692
+ });
3693
+ });
3694
+ c.on("session/event", (session, event) => {
3695
+ if (event.type !== "plan/mode") return;
3696
+ firstPolicySeen.delete(session);
3697
+ if (!settings.effective(session.header?.cwd).enabled) {
3698
+ bridge.clearSession(String(session.id));
3699
+ readOnly.deactivate(session, c.sandboxPolicy);
3700
+ return;
3701
+ }
3702
+ if (!event.data.active) {
3703
+ bridge.clearSession(String(session.id));
3704
+ readOnly.deactivate(session, c.sandboxPolicy);
3705
+ }
3706
+ });
3707
+ c.on("agent/session-start", ({ agent }) => {
3708
+ if (!isEnabled(agent)) return;
3709
+ orchestration.reconcileSession(agent).catch((error) => c.logger?.warn?.("plan-orchestrator recovery reconcile failed: %o", error));
3710
+ });
3711
+ c.inject(["commands"], (scope) => scope.effect(() => installIssueCommand(scope, {
3712
+ settings: (cwd) => settings.effective(cwd),
3713
+ orchestration
3714
+ }), "plan-orchestrator: external issue command"));
3715
+ c.inject(["connection"], (scope) => scope.effect(() => {
3716
+ let disposed = false;
3717
+ let disposeTransport;
3718
+ import("./rpc-server-CGs2ndXP.js").then(({ registerRpc }) => {
3719
+ if (disposed) return;
3720
+ disposeTransport = registerRpc(scope.connection, {
3721
+ ctx: scope,
3722
+ isEnabled: () => settings.get().enabled,
3723
+ canResume: () => settings.get().recovery.allowSafeResume,
3724
+ runList: ({ sessionId }) => orchestration.list(sessionId),
3725
+ runDetail: ({ runId }) => orchestration.detail(runId),
3726
+ runCancel: ({ runId }) => orchestration.cancel(runId),
3727
+ runResume: ({ runId }) => orchestration.resume(runId),
3728
+ runCleanup: ({ runId }) => orchestration.cleanup(runId),
3729
+ externalPreflight: async (body) => {
3730
+ const cwd = typeof body.cwd === "string" ? await repoRoot(body.cwd) : void 0;
3731
+ if (!cwd || typeof body.repository !== "string") throw new Error("cwd and repository required");
3732
+ return ghPreflight(cwd, body.repository);
3733
+ },
3734
+ diagnostics: async (body) => {
3735
+ const cwd = typeof body.cwd === "string" ? body.cwd : void 0;
3736
+ let gitAvailable = await commandAvailable("git", ["--version"]);
3737
+ let repo = false;
3738
+ if (gitAvailable && cwd) try {
3739
+ await repoRoot(cwd);
3740
+ repo = true;
3741
+ } catch {}
3742
+ return {
3743
+ pluginVersion: "1.0.0",
3744
+ dshVersion: packageVersion("@deepseek-ai/dsh-plan-mode"),
3745
+ compatibility: {
3746
+ supported: ["0.1.5-rc.1"],
3747
+ preview: ["0.1.5-rc.2"]
3748
+ },
3749
+ nativePlanMode: hasService(scope, "planMode"),
3750
+ sdkAvailable: packageVersion("@deepseek-ai/dsh-sdk-client") !== void 0,
3751
+ gitAvailable,
3752
+ gitRepository: repo,
3753
+ lspAvailable: hasService(scope, "lsp"),
3754
+ ghAvailable: await commandAvailable("gh", ["--version"]),
3755
+ settingsWritable: settings.writable(),
3756
+ storage: store.root,
3757
+ readOnlyDegraded: typeof body.sessionId === "string" ? readOnly.degraded(body.sessionId) : void 0
3758
+ };
3759
+ }
3760
+ });
3761
+ }).catch((error) => scope.logger?.error?.("plan-orchestrator RPC registration failed: %o", error));
3762
+ return () => {
3763
+ disposed = true;
3764
+ disposeTransport?.();
3765
+ };
3766
+ }, "plan-orchestrator: rpc"));
3767
+ settings.watch((value) => {
3768
+ if (!value.enabled) {
3769
+ orchestration.cancelAll("Plan Orchestrator disabled in settings").catch((error) => c.logger?.warn?.("plan-orchestrator disable cancellation failed: %o", error));
3770
+ return;
3771
+ }
3772
+ for (const role of Object.values(value.roles)) if (role.mode === "fixed") validateFixedRoute(c.llm, role).catch((error) => c.logger?.warn?.("plan-orchestrator fixed route unavailable: %s", error.message));
3773
+ });
3774
+ }
3775
+ //#endregion
3776
+ export { apply, inject, name };