sortie-dogs 0.12.3 → 0.12.5
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/README.md +6 -7
- package/dist/core/failure-swarm-runtime.d.ts +1 -1
- package/dist/core/initialize.js +58 -4
- package/dist/core/operator-mission.d.ts +8 -0
- package/dist/core/operator-mission.js +70 -6
- package/dist/core/operator-runtime.d.ts +3 -1
- package/dist/core/operator-runtime.js +32 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/plugin/index.js +86 -2
- package/dist/plugin/opencode.d.ts +7 -7
- package/dist/plugin/opencode.js +7 -7
- package/dist/plugin/profiled.js +45 -6
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -42,8 +42,8 @@ npm install --save-dev sortie-dogs
|
|
|
42
42
|
npx sortie-dogs init .
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
The package retains the `v010` profile/namespace for compatibility.
|
|
46
|
-
two-level subagent depth to `.opencode/opencode.json`, preserving existing
|
|
45
|
+
The package retains the `v010` profile/namespace for compatibility. `init` adds the OpenCode V2 plugin and the required
|
|
46
|
+
two-level subagent depth to `.opencode/opencode.json(c)`, preserving existing settings:
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
49
|
{
|
|
@@ -62,8 +62,8 @@ Selecting `dog-operator` directly starts the same workflow. `dog-operator` is th
|
|
|
62
62
|
only user-facing v0.10 authority. `dogs-coordinator` and every `*-v010` role are
|
|
63
63
|
internal children and must not be selected as task entry points.
|
|
64
64
|
|
|
65
|
-
`init` installs runtime assets; the `plugins` entry loads runtime enforcement and
|
|
66
|
-
model routing.
|
|
65
|
+
`init` installs runtime assets and merges the required OpenCode settings; the `plugins` entry loads runtime enforcement and
|
|
66
|
+
model routing. A new session alone does not reload an updated
|
|
67
67
|
plugin process, so restart OpenCode after installation or upgrade.
|
|
68
68
|
|
|
69
69
|
## v0.12.0 workflow
|
|
@@ -262,9 +262,8 @@ npm install --global sortie-dogs
|
|
|
262
262
|
sortie-dogs init --global --profile v010
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
default agent or merge user settings.
|
|
265
|
+
Global initialization also merges `sortie-dogs` and `experimental.subagent_depth: 2` into the
|
|
266
|
+
global OpenCode config, preserving unrelated settings and the default agent.
|
|
268
267
|
|
|
269
268
|
## Updates and removal
|
|
270
269
|
|
|
@@ -67,8 +67,8 @@ export declare class FailureSwarmRuntime {
|
|
|
67
67
|
probe_iterations: number;
|
|
68
68
|
model_attempts: number;
|
|
69
69
|
}>;
|
|
70
|
-
capsule_id: string;
|
|
71
70
|
proposal: string;
|
|
71
|
+
capsule_id: string;
|
|
72
72
|
recovery_kind: Exclude<import("./run-flight-ledger.js").RecoveryKind, "read_only_diagnosis">;
|
|
73
73
|
diagnosis_id: string;
|
|
74
74
|
plan_id: string;
|
package/dist/core/initialize.js
CHANGED
|
@@ -11,6 +11,7 @@ import { constants } from "node:fs";
|
|
|
11
11
|
import { lstat, mkdir, open, readFile, realpath, rm, rmdir, stat } from "node:fs/promises";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
14
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
14
15
|
import { RUNTIME_PROFILES } from "./runtime-profile.js";
|
|
15
16
|
const assets = await import(__rewriteRelativeImportExtension(`../runtime-assets.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
16
17
|
const { runtimeAssets } = assets;
|
|
@@ -348,6 +349,46 @@ function layoutLegacyPath(asset, layout) {
|
|
|
348
349
|
? asset.relativePath.slice(`${OPEN_CODE_DIRECTORY}/`.length)
|
|
349
350
|
: asset.relativePath;
|
|
350
351
|
}
|
|
352
|
+
async function v010Config(root, global) {
|
|
353
|
+
const prefix = global ? "" : `${OPEN_CODE_DIRECTORY}/`;
|
|
354
|
+
const json = `${prefix}opencode.json`, jsonc = `${prefix}opencode.jsonc`;
|
|
355
|
+
const presentJSON = await assertSafeExistingPath(root, json, true);
|
|
356
|
+
const presentJSONC = await assertSafeExistingPath(root, jsonc, true);
|
|
357
|
+
if (presentJSON && presentJSONC)
|
|
358
|
+
throw new ProjectInitializationError("conflict", "Both OpenCode JSON and JSONC configs exist; choose one before initialization.");
|
|
359
|
+
const relativePath = presentJSONC ? jsonc : json;
|
|
360
|
+
const previous = presentJSONC || presentJSON ? await readFile(resolve(root, relativePath)) : undefined;
|
|
361
|
+
let content = previous?.toString("utf8") ?? "{}\n";
|
|
362
|
+
const errors = [];
|
|
363
|
+
const config = parse(content, errors, { allowTrailingComma: true });
|
|
364
|
+
if (errors.length || config === null || typeof config !== "object" || Array.isArray(config)) {
|
|
365
|
+
throw new ProjectInitializationError("conflict", `OpenCode config ${relativePath} must be a valid JSON/JSONC object.`);
|
|
366
|
+
}
|
|
367
|
+
const current = config;
|
|
368
|
+
const plugins = current.plugins;
|
|
369
|
+
if (plugins !== undefined && (!Array.isArray(plugins) || !plugins.every(item => typeof item === "string" ||
|
|
370
|
+
(item !== null && typeof item === "object" && !Array.isArray(item) && typeof item.package === "string")))) {
|
|
371
|
+
throw new ProjectInitializationError("conflict", `OpenCode config ${relativePath} has an invalid plugins list.`);
|
|
372
|
+
}
|
|
373
|
+
if (current.experimental !== undefined && (current.experimental === null || typeof current.experimental !== "object" ||
|
|
374
|
+
Array.isArray(current.experimental)))
|
|
375
|
+
throw new ProjectInitializationError("conflict", `OpenCode config ${relativePath} has invalid experimental settings.`);
|
|
376
|
+
const depth = current.experimental?.subagent_depth;
|
|
377
|
+
if (depth !== undefined && (!Number.isSafeInteger(depth) || depth < 0)) {
|
|
378
|
+
throw new ProjectInitializationError("conflict", `OpenCode config ${relativePath} has an invalid subagent depth.`);
|
|
379
|
+
}
|
|
380
|
+
const options = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
|
|
381
|
+
const update = (path, value) => { content = applyEdits(content, modify(content, path, value, options)); };
|
|
382
|
+
if (!plugins?.some(item => item === "sortie-dogs" ||
|
|
383
|
+
(item !== null && typeof item === "object" && item.package === "sortie-dogs"))) {
|
|
384
|
+
update(["plugins"], [...(plugins ?? []), "sortie-dogs"]);
|
|
385
|
+
}
|
|
386
|
+
if (depth === undefined || depth < 2)
|
|
387
|
+
update(["experimental", "subagent_depth"], 2);
|
|
388
|
+
if (previous?.toString("utf8") === content)
|
|
389
|
+
return undefined;
|
|
390
|
+
return { entry: { relativePath, content }, previous };
|
|
391
|
+
}
|
|
351
392
|
async function initializeRoot(requestedRoot, layout, installAssets = runtimeAssets) {
|
|
352
393
|
const root = resolve(requestedRoot);
|
|
353
394
|
const legacyAssets = layout.legacyAssets ?? LEGACY_RUNTIME_ASSETS;
|
|
@@ -355,6 +396,7 @@ async function initializeRoot(requestedRoot, layout, installAssets = runtimeAsse
|
|
|
355
396
|
if (rootInfo === undefined || !rootInfo.isDirectory() || rootInfo.isSymbolicLink()) {
|
|
356
397
|
throw new ProjectInitializationError("invalid-project", layout.invalidRootMessage);
|
|
357
398
|
}
|
|
399
|
+
const configUpdate = layout.configureV010 ? await v010Config(root, layout.assetPrefix === "") : undefined;
|
|
358
400
|
const version = assetVersion(installAssets);
|
|
359
401
|
const assetEntries = installAssets.map(({ installPath, content }) => ({
|
|
360
402
|
relativePath: safeAssetPath(installPath, layout.assetPrefix),
|
|
@@ -381,7 +423,7 @@ async function initializeRoot(requestedRoot, layout, installAssets = runtimeAsse
|
|
|
381
423
|
const markerText = existing[markerIndex];
|
|
382
424
|
const matches = (entry, index) => existing[index]?.equals(Buffer.from(entry.content)) ?? false;
|
|
383
425
|
const assetsMatch = assetEntries.every(matches);
|
|
384
|
-
if (markerText !== undefined && parseMarker(markerText.toString("utf8")) === version && assetsMatch) {
|
|
426
|
+
if (markerText !== undefined && parseMarker(markerText.toString("utf8")) === version && assetsMatch && !configUpdate) {
|
|
385
427
|
if (layout.controlIgnore ?? !layout.preserveAllLegacy)
|
|
386
428
|
await ensureProjectLunaControlIgnore(root);
|
|
387
429
|
return {
|
|
@@ -449,6 +491,18 @@ async function initializeRoot(requestedRoot, layout, installAssets = runtimeAsse
|
|
|
449
491
|
});
|
|
450
492
|
}
|
|
451
493
|
}
|
|
494
|
+
if (configUpdate) {
|
|
495
|
+
const { entry, previous } = configUpdate;
|
|
496
|
+
const parent = dirname(entry.relativePath).replaceAll("\\", "/");
|
|
497
|
+
if (parent !== ".")
|
|
498
|
+
await ensureDirectory(root, parent, createdDirectories);
|
|
499
|
+
if (previous === undefined)
|
|
500
|
+
await createFile(resolve(root, entry.relativePath), entry.content, createdFiles);
|
|
501
|
+
else
|
|
502
|
+
await overwriteFileSafely(root, entry.relativePath, entry.content, () => {
|
|
503
|
+
modifiedFiles.push({ relativePath: entry.relativePath, content: previous });
|
|
504
|
+
});
|
|
505
|
+
}
|
|
452
506
|
for (const { asset } of removableLegacyFiles) {
|
|
453
507
|
const state = await readableOwnedLegacyFile(root, asset);
|
|
454
508
|
if (!Buffer.isBuffer(state)) {
|
|
@@ -478,7 +532,7 @@ async function initializeRoot(requestedRoot, layout, installAssets = runtimeAsse
|
|
|
478
532
|
return {
|
|
479
533
|
status: "installed",
|
|
480
534
|
version,
|
|
481
|
-
installedPaths: entries.map(({ relativePath }) => relativePath),
|
|
535
|
+
installedPaths: [...entries.map(({ relativePath }) => relativePath), ...(configUpdate ? [configUpdate.entry.relativePath] : [])],
|
|
482
536
|
preservedLegacyPaths,
|
|
483
537
|
};
|
|
484
538
|
}
|
|
@@ -502,7 +556,7 @@ export async function resolveGlobalConfigRoot(env = process.env, home = homedir(
|
|
|
502
556
|
return resolve(env.XDG_CONFIG_HOME, "opencode");
|
|
503
557
|
return resolve(home, ".config", "opencode");
|
|
504
558
|
}
|
|
505
|
-
/** Installs the packaged runtime into one existing project
|
|
559
|
+
/** Installs the packaged runtime into one existing project, preserving unrelated user settings. */
|
|
506
560
|
async function profileInstallation(id, global) {
|
|
507
561
|
if (!Object.hasOwn(RUNTIME_PROFILES, id))
|
|
508
562
|
throw new ProjectInitializationError("invalid-project", "Unknown runtime profile.");
|
|
@@ -515,7 +569,7 @@ async function profileInstallation(id, global) {
|
|
|
515
569
|
return { layout: { ...(global ? GLOBAL_LAYOUT : PROJECT_LAYOUT),
|
|
516
570
|
markerPath: global ? profile.markerFile : `${OPEN_CODE_DIRECTORY}/${profile.markerFile}`,
|
|
517
571
|
preserveAllLegacy: false, controlIgnore: false, legacyAssets: V010_ROLE_NAME_LEGACY_ASSETS,
|
|
518
|
-
renamedTargets: ["agent/dog-operator.md", "agent/dogs-coordinator.md"] }, assets: module.runtimeAssets };
|
|
572
|
+
renamedTargets: ["agent/dog-operator.md", "agent/dogs-coordinator.md"], configureV010: true }, assets: module.runtimeAssets };
|
|
519
573
|
}
|
|
520
574
|
export async function initializeProject(projectRoot = process.cwd(), profile = "stable") {
|
|
521
575
|
const install = await profileInstallation(profile, false);
|
|
@@ -22,6 +22,8 @@ export interface OperatorMission {
|
|
|
22
22
|
callID: string | null;
|
|
23
23
|
dispatchOpen: boolean;
|
|
24
24
|
runID: string | null;
|
|
25
|
+
/** A cancelled run replaced by a later real user turn; never reuse its acceptance or evidence. */
|
|
26
|
+
supersededRunID?: string;
|
|
25
27
|
plans: number;
|
|
26
28
|
progress: {
|
|
27
29
|
unit: string;
|
|
@@ -51,6 +53,10 @@ export declare class OperatorMissionRuntime {
|
|
|
51
53
|
constructor(projectRoot: string, profile: RuntimeProfile);
|
|
52
54
|
private file;
|
|
53
55
|
private load;
|
|
56
|
+
/** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
|
|
57
|
+
private loadMission;
|
|
58
|
+
/** Find exactly one archived cancelled mission that owned this run; ambiguity never grants recovery. */
|
|
59
|
+
archivedRun(root: string, runID: string): Promise<OperatorMission | undefined>;
|
|
54
60
|
private save;
|
|
55
61
|
private serial;
|
|
56
62
|
read(root: string): Promise<OperatorMission | undefined>;
|
|
@@ -60,6 +66,8 @@ export declare class OperatorMissionRuntime {
|
|
|
60
66
|
update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
|
|
61
67
|
task(state: OperatorMission): OperatorTask;
|
|
62
68
|
admit(root: string, callID: string, args: Record<string, unknown>): Promise<OperatorMission>;
|
|
69
|
+
/** Reopen only the exact dispatch whose native Task has a terminal record after a server restart. */
|
|
70
|
+
reconcileFinishedDispatch(root: string, missionID: string, callID: string): Promise<OperatorMission>;
|
|
63
71
|
claim(root: string, child: string, prompt: string): Promise<string>;
|
|
64
72
|
brief(state: OperatorMission): string;
|
|
65
73
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { normalizeRelativePath } from "./path.js";
|
|
5
5
|
import { parseOperatorPlan } from "./operator-runtime.js";
|
|
@@ -40,6 +40,59 @@ export class OperatorMissionRuntime {
|
|
|
40
40
|
throw error;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
|
|
44
|
+
async loadMission(root) {
|
|
45
|
+
const state = await this.load(this.file(root));
|
|
46
|
+
if (!state || state.supersededRunID !== undefined || state.runID !== null ||
|
|
47
|
+
!["open", "running", "submitted"].includes(state.phase) || state.requests.length === 0)
|
|
48
|
+
return state;
|
|
49
|
+
const directory = join(this.projectRoot, this.profile.stateDirectory, "missions");
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(directory);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT")
|
|
56
|
+
return state;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const prefix = `${digest(root)}.mission-`;
|
|
60
|
+
const candidates = entries.filter(name => name.startsWith(prefix) && /^mission-[a-f0-9-]+\.json$/u.test(name.slice(digest(root).length + 1)));
|
|
61
|
+
const matches = [];
|
|
62
|
+
for (const name of candidates) {
|
|
63
|
+
const previous = await this.load(join(directory, name));
|
|
64
|
+
if (previous?.root === root && previous.phase === "cancelled" && previous.runID !== null &&
|
|
65
|
+
previous.requests[0]?.id !== state.requests[0].id &&
|
|
66
|
+
previous.requests.some(request => request.id === state.requests[0].id))
|
|
67
|
+
matches.push(previous);
|
|
68
|
+
}
|
|
69
|
+
if (matches.length === 1)
|
|
70
|
+
state.supersededRunID = matches[0].runID;
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
/** Find exactly one archived cancelled mission that owned this run; ambiguity never grants recovery. */
|
|
74
|
+
async archivedRun(root, runID) {
|
|
75
|
+
const directory = join(this.projectRoot, this.profile.stateDirectory, "missions");
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = await readdir(directory);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code === "ENOENT")
|
|
82
|
+
return undefined;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
const prefix = `${digest(root)}.mission-`;
|
|
86
|
+
const matches = [];
|
|
87
|
+
for (const name of entries.filter(name => name.startsWith(prefix) &&
|
|
88
|
+
/^mission-[a-f0-9-]+\.json$/u.test(name.slice(digest(root).length + 1)))) {
|
|
89
|
+
const state = await this.load(join(directory, name));
|
|
90
|
+
if (state?.root === root && state.phase === "cancelled" && state.runID === runID &&
|
|
91
|
+
state.coordinator !== null && state.callID !== null)
|
|
92
|
+
matches.push(state);
|
|
93
|
+
}
|
|
94
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
95
|
+
}
|
|
43
96
|
async save(file, value) {
|
|
44
97
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
45
98
|
await mkdir(join(this.projectRoot, this.profile.stateDirectory, "missions"), { recursive: true });
|
|
@@ -64,7 +117,7 @@ export class OperatorMissionRuntime {
|
|
|
64
117
|
}
|
|
65
118
|
async read(root) {
|
|
66
119
|
await this.writes.get(root);
|
|
67
|
-
const state = await this.
|
|
120
|
+
const state = await this.loadMission(root);
|
|
68
121
|
if (state && (state.version !== "0.12" || state.root !== root))
|
|
69
122
|
throw new Error("mission-state-invalid");
|
|
70
123
|
return state;
|
|
@@ -81,7 +134,7 @@ export class OperatorMissionRuntime {
|
|
|
81
134
|
await this.serial(root, async () => {
|
|
82
135
|
// Captured before prompt rewriting, including exact whitespace. Never ask a model to recopy it.
|
|
83
136
|
await this.save(this.file(root, ".request"), request);
|
|
84
|
-
const state = await this.
|
|
137
|
+
const state = await this.loadMission(root);
|
|
85
138
|
if (state && !["completed", "cancelled"].includes(state.phase) && !state.requests.some(item => item.id === request.id)) {
|
|
86
139
|
state.requests.push(request);
|
|
87
140
|
await this.save(this.file(root), state);
|
|
@@ -97,7 +150,7 @@ export class OperatorMissionRuntime {
|
|
|
97
150
|
const request = await this.load(this.file(root, ".request"));
|
|
98
151
|
if (!request)
|
|
99
152
|
throw new Error("mission-original-request-unavailable");
|
|
100
|
-
const previous = await this.
|
|
153
|
+
const previous = await this.loadMission(root);
|
|
101
154
|
if (previous && !["completed", "cancelled"].includes(previous.phase)) {
|
|
102
155
|
if (previous.requirements.some((item, index) => requirements[index] !== item.text)) {
|
|
103
156
|
throw new Error("mission-requirements-preserved: keep the existing ordered requirements and append user additions");
|
|
@@ -110,14 +163,16 @@ export class OperatorMissionRuntime {
|
|
|
110
163
|
await this.save(this.file(root, `.${previous.id}`), previous);
|
|
111
164
|
const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
|
|
112
165
|
requirements: requirements.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "open",
|
|
113
|
-
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null
|
|
166
|
+
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
|
|
167
|
+
...(previous?.phase === "cancelled" && previous.runID !== null && request.id !== previous.requests[0]?.id
|
|
168
|
+
? { supersededRunID: previous.runID } : {}) };
|
|
114
169
|
await this.save(this.file(root), state);
|
|
115
170
|
return state;
|
|
116
171
|
});
|
|
117
172
|
}
|
|
118
173
|
update(root, change) {
|
|
119
174
|
return this.serial(root, async () => {
|
|
120
|
-
const state = await this.
|
|
175
|
+
const state = await this.loadMission(root);
|
|
121
176
|
if (!state)
|
|
122
177
|
throw new Error("mission-missing");
|
|
123
178
|
change(state);
|
|
@@ -144,6 +199,15 @@ export class OperatorMissionRuntime {
|
|
|
144
199
|
state.submission = null;
|
|
145
200
|
});
|
|
146
201
|
}
|
|
202
|
+
/** Reopen only the exact dispatch whose native Task has a terminal record after a server restart. */
|
|
203
|
+
reconcileFinishedDispatch(root, missionID, callID) {
|
|
204
|
+
return this.update(root, state => {
|
|
205
|
+
if (state.id !== missionID || state.callID !== callID || !state.dispatchOpen ||
|
|
206
|
+
["cancelled", "completed"].includes(state.phase))
|
|
207
|
+
throw new Error("mission-dispatch-reconciliation-stale");
|
|
208
|
+
state.dispatchOpen = false;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
147
211
|
async claim(root, child, prompt) {
|
|
148
212
|
const state = await this.update(root, state => {
|
|
149
213
|
if (state.phase !== "running" || !state.dispatchOpen || !state.callID ||
|
|
@@ -172,6 +172,8 @@ export interface OperatorState {
|
|
|
172
172
|
readonly sourceRefs: readonly string[];
|
|
173
173
|
readonly createdAt: string;
|
|
174
174
|
readonly parentRunID: string | null;
|
|
175
|
+
/** Archived cancelled run replaced by a later user-authorized mission. */
|
|
176
|
+
readonly supersededRunID?: string;
|
|
175
177
|
readonly priorAcceptedUnits: readonly OperatorAcceptanceAnchor[];
|
|
176
178
|
readonly remediationParent: {
|
|
177
179
|
readonly taskID: string;
|
|
@@ -243,7 +245,7 @@ export declare class OperatorRuntime {
|
|
|
243
245
|
prepareMission(root: string, raw: unknown, dispatcher?: {
|
|
244
246
|
sessionID: string;
|
|
245
247
|
callID: string;
|
|
246
|
-
}): Promise<OperatorState>;
|
|
248
|
+
}, supersededRunID?: string): Promise<OperatorState>;
|
|
247
249
|
retireMissionRun(root: string): Promise<void>;
|
|
248
250
|
private prepareOnce;
|
|
249
251
|
operatorTask(state: OperatorState): OperatorTask;
|
|
@@ -299,7 +299,8 @@ export class OperatorRuntime {
|
|
|
299
299
|
!identifier(state.runID) || !Array.isArray(state.units) || state.units.length > OPERATOR_LIMITS.units ||
|
|
300
300
|
!strings(state.acceptance, true) || !Array.isArray(state.acceptanceProof) || state.acceptanceProof.length !== state.acceptance.length ||
|
|
301
301
|
state.acceptanceProof.some(ids => !strings(ids, true)) || state.acceptanceFingerprint !== acceptanceContinuityFingerprint(state.acceptance) ||
|
|
302
|
-
(state.parentRunID !== null && !identifier(state.parentRunID)) ||
|
|
302
|
+
(state.parentRunID !== null && !identifier(state.parentRunID)) ||
|
|
303
|
+
(state.supersededRunID !== undefined && !identifier(state.supersededRunID)) || !Array.isArray(state.priorAcceptedUnits) ||
|
|
303
304
|
state.priorAcceptedUnits.some(anchor => !record(anchor) || !identifier(anchor.taskID) || typeof anchor.handoffPath !== "string" || !isAbsolute(anchor.handoffPath) ||
|
|
304
305
|
typeof anchor.handoffHash !== "string" || !/^[a-f0-9]{64}$/u.test(anchor.handoffHash)) ||
|
|
305
306
|
(state.remediationParent !== null && (!record(state.remediationParent) || !identifier(state.remediationParent.taskID) ||
|
|
@@ -677,8 +678,8 @@ export class OperatorRuntime {
|
|
|
677
678
|
}
|
|
678
679
|
}
|
|
679
680
|
/** Mission authority is supplied only by the owning profile, never by a model-authored plan. */
|
|
680
|
-
prepareMission(root, raw, dispatcher) {
|
|
681
|
-
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher }));
|
|
681
|
+
prepareMission(root, raw, dispatcher, supersededRunID) {
|
|
682
|
+
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID }));
|
|
682
683
|
}
|
|
683
684
|
retireMissionRun(root) {
|
|
684
685
|
return this.serial(root, async () => {
|
|
@@ -718,18 +719,30 @@ export class OperatorRuntime {
|
|
|
718
719
|
immutableReplacement = { ...immutableReplacement, goal_declaration: declaration };
|
|
719
720
|
}
|
|
720
721
|
const plan = parseOperatorPlan(immutableReplacement);
|
|
722
|
+
const validatedPlanHash = hash(JSON.stringify(plan));
|
|
723
|
+
if (mission?.supersededRunID !== undefined && previous?.supersededRunID === mission.supersededRunID &&
|
|
724
|
+
previous.planHash === validatedPlanHash && previous.phase !== "cancelled")
|
|
725
|
+
return previous;
|
|
726
|
+
const superseding = mission?.supersededRunID !== undefined && previous?.runID === mission.supersededRunID &&
|
|
727
|
+
previous.phase === "cancelled";
|
|
728
|
+
if (mission?.supersededRunID !== undefined && !superseding)
|
|
729
|
+
throw new Error("mission-superseded-run-mismatch");
|
|
730
|
+
if (superseding && previous && (previous.decision !== "explicit-cancellation" || previous.gitLifecycle !== null ||
|
|
731
|
+
previous.repairResidualPaths.length > 0 || previous.priorAcceptedUnits.length > 0 ||
|
|
732
|
+
previous.units.some(unit => unit.childSessionID !== null || unit.status === "succeeded"))) {
|
|
733
|
+
throw new Error("mission-superseded-run-has-work: reconcile prior workers and evidence before changing acceptance");
|
|
734
|
+
}
|
|
721
735
|
if (plan.git_lifecycle !== undefined) {
|
|
722
736
|
await ensureGitManagedStateExcluded(this.projectRoot, this.profile, this.gitPath);
|
|
723
737
|
}
|
|
724
|
-
const planHash = hash(JSON.stringify(plan));
|
|
725
738
|
if (previous && !["completed", "cancelled"].includes(previous.phase)) {
|
|
726
|
-
if (previous.planHash ===
|
|
739
|
+
if (previous.planHash === validatedPlanHash)
|
|
727
740
|
return previous;
|
|
728
741
|
throw new Error("operator-active-contract-immutable");
|
|
729
742
|
}
|
|
730
743
|
// Cancellation stops execution, not the accepted user order. A replacement
|
|
731
744
|
// plan must carry the original ordered criteria; only a completed root clears them.
|
|
732
|
-
const parent = previous?.phase === "cancelled" ? previous : undefined;
|
|
745
|
+
const parent = !superseding && previous?.phase === "cancelled" ? previous : undefined;
|
|
733
746
|
if (parent?.decision === "operator-contract-repair-unavailable-after-cancel") {
|
|
734
747
|
// The diagnosed transients, not the cancellation itself, block a replacement. Once they are
|
|
735
748
|
// gone the same accepted order may proceed; while they remain the refusal names them exactly.
|
|
@@ -878,9 +891,9 @@ export class OperatorRuntime {
|
|
|
878
891
|
childSessionID: null, evidence: [], resultClass: null, repairValidationAttempts: 0, repairValidation: null });
|
|
879
892
|
}
|
|
880
893
|
const gitLifecycle = await this.createGitLifecycle(plan, remediationHead);
|
|
881
|
-
const state = { schema_version: "0.1", profile: this.profile.id, rootSessionID: root, runID, planHash,
|
|
894
|
+
const state = { schema_version: "0.1", profile: this.profile.id, rootSessionID: root, runID, planHash: validatedPlanHash,
|
|
882
895
|
acceptance: plan.acceptance, acceptanceProof: plan.acceptance_proof, acceptanceFingerprint, sourceRefs: plan.source_refs, createdAt: new Date().toISOString(),
|
|
883
|
-
parentRunID: parent?.runID ?? null, priorAcceptedUnits,
|
|
896
|
+
parentRunID: parent?.runID ?? null, ...(superseding ? { supersededRunID: previous.runID } : {}), priorAcceptedUnits,
|
|
884
897
|
remediationParent: remediationTaskID && remediationHead && parent?.gitLifecycle ? {
|
|
885
898
|
taskID: remediationTaskID, committedHead: remediationHead, runID: parent.runID,
|
|
886
899
|
acceptanceFingerprint: parent.acceptanceFingerprint, approvedWriteUnion: parent.gitLifecycle.writeUnion,
|
|
@@ -908,6 +921,17 @@ export class OperatorRuntime {
|
|
|
908
921
|
await handle.close();
|
|
909
922
|
}
|
|
910
923
|
}
|
|
924
|
+
if (superseding && previous) {
|
|
925
|
+
const archive = `${this.file(root)}.${previous.runID}.archive`;
|
|
926
|
+
try {
|
|
927
|
+
await writeFile(archive, JSON.stringify(previous), { flag: "wx", mode: 0o600 });
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
if (error.code !== "EEXIST" ||
|
|
931
|
+
await readFile(archive, "utf8") !== JSON.stringify(previous))
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
911
935
|
await this.save(state);
|
|
912
936
|
return state;
|
|
913
937
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { default } from "./plugin/v2.js";
|
|
1
2
|
export { lintHandoff } from "./core/validate-semantics.js";
|
|
2
3
|
export { validateWorktreeParallelContract } from "./core/validate-worktree-parallel.js";
|
|
3
4
|
export { LUNA_FABRIC_CONTRACT_VERSION, admitLunaFabric, } from "./core/luna-fabric-contract.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// OpenCode V2 loads the package root named in `plugins`; keep the public named API intact.
|
|
2
|
+
export { default } from "./plugin/v2.js";
|
|
1
3
|
export { lintHandoff } from "./core/validate-semantics.js";
|
|
2
4
|
export { validateWorktreeParallelContract } from "./core/validate-worktree-parallel.js";
|
|
3
5
|
export { LUNA_FABRIC_CONTRACT_VERSION, admitLunaFabric, } from "./core/luna-fabric-contract.js";
|
package/dist/plugin/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
|
13
13
|
import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES, GOAL_CRITERION_ENUMS, expandGoalDeclaration } from "../core/goal-declaration-format.js";
|
|
14
14
|
import { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, } from "../core/acceptance-continuity.js";
|
|
15
15
|
import { resolveGlobalConfigRoot } from "../core/initialize.js";
|
|
16
|
+
import { OperatorMissionRuntime } from "../core/operator-mission.js";
|
|
16
17
|
import { admitLunaFabric } from "../core/luna-fabric-contract.js";
|
|
17
18
|
import { summarizeExperienceEvidence } from "../core/experience-evidence-summary.js";
|
|
18
19
|
import { selectExperienceRoute } from "../core/experience-route-policy.js";
|
|
@@ -1609,6 +1610,82 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1609
1610
|
const data = isRecord(response) ? response.data : undefined;
|
|
1610
1611
|
if (!Array.isArray(data))
|
|
1611
1612
|
return;
|
|
1613
|
+
const rootHistory = data;
|
|
1614
|
+
async function terminalCancelledMissionTask(unitID, reservationID, goalID) {
|
|
1615
|
+
const runID = unitID.replace(/-[1-9][0-9]*$/u, "");
|
|
1616
|
+
if (runID === unitID || !/^operator-[a-f0-9-]+$/u.test(runID) ||
|
|
1617
|
+
input.client?.session?.get === undefined)
|
|
1618
|
+
return undefined;
|
|
1619
|
+
const sessionAPI = input.client.session;
|
|
1620
|
+
const childrenMethod = sessionAPI.children;
|
|
1621
|
+
const getMethod = sessionAPI.get;
|
|
1622
|
+
const mission = await new OperatorMissionRuntime(input.directory, runtimeProfile).archivedRun(root, runID);
|
|
1623
|
+
if (!mission || !rootHistory.some(message => isRecord(message) && isRecord(message.info) &&
|
|
1624
|
+
message.info.sessionID === root && message.info.role === "assistant" && Array.isArray(message.parts) &&
|
|
1625
|
+
message.parts.some(part => isRecord(part) && part.type === "tool" && part.tool === "task" &&
|
|
1626
|
+
part.callID === mission.callID && isRecord(part.state) && part.state.status === "completed" &&
|
|
1627
|
+
isRecord(part.state.input) && typeof part.state.input.prompt === "string" &&
|
|
1628
|
+
part.state.input.prompt.startsWith("SORTIE_MISSION_REF ") && (() => {
|
|
1629
|
+
try {
|
|
1630
|
+
const ref = JSON.parse(part.state.input.prompt.slice("SORTIE_MISSION_REF ".length));
|
|
1631
|
+
return isRecord(ref) && ref.r === root && ref.m === mission.id;
|
|
1632
|
+
}
|
|
1633
|
+
catch {
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
})())))
|
|
1637
|
+
return undefined;
|
|
1638
|
+
const coordinatorResponse = await getMethod.call(sessionAPI, { path: { id: mission.coordinator }, query: { directory: input.directory } });
|
|
1639
|
+
const coordinatorInfo = isRecord(coordinatorResponse) ? coordinatorResponse.data : undefined;
|
|
1640
|
+
if (!isRecord(coordinatorInfo) || coordinatorInfo.id !== mission.coordinator ||
|
|
1641
|
+
coordinatorInfo.parentID !== root || coordinatorInfo.agent !== "dogs-coordinator" ||
|
|
1642
|
+
coordinatorInfo.outcome !== "succeeded")
|
|
1643
|
+
return undefined;
|
|
1644
|
+
if (childrenMethod !== undefined) {
|
|
1645
|
+
const childrenResponse = await childrenMethod.call(sessionAPI, { path: { id: root }, query: { directory: input.directory } });
|
|
1646
|
+
const children = isRecord(childrenResponse) && Array.isArray(childrenResponse.data) ? childrenResponse.data : [];
|
|
1647
|
+
if (!children.some(child => isRecord(child) && child.id === mission.coordinator &&
|
|
1648
|
+
child.parentID === root && child.agent === "dogs-coordinator"))
|
|
1649
|
+
return undefined;
|
|
1650
|
+
}
|
|
1651
|
+
const coordinator = mission.coordinator;
|
|
1652
|
+
const historyResponse = await messages.call(sessionAPI, { path: { id: coordinator }, query: { directory: input.directory } });
|
|
1653
|
+
const history = isRecord(historyResponse) && Array.isArray(historyResponse.data) ? historyResponse.data : [];
|
|
1654
|
+
const candidates = [];
|
|
1655
|
+
for (const message of history) {
|
|
1656
|
+
if (!isRecord(message) || !isRecord(message.info) || message.info.role !== "assistant" ||
|
|
1657
|
+
message.info.sessionID !== coordinator || !Array.isArray(message.parts))
|
|
1658
|
+
continue;
|
|
1659
|
+
for (const part of message.parts) {
|
|
1660
|
+
if (!isRecord(part) || part.type !== "tool" || part.tool !== "task" ||
|
|
1661
|
+
!isRecord(part.state) || part.state.status !== "error" || !isRecord(part.state.input) ||
|
|
1662
|
+
typeof part.state.input.prompt !== "string" || !part.state.input.prompt.startsWith("SORTIE_OPERATOR_TASK_REF ") ||
|
|
1663
|
+
typeof part.callID !== "string")
|
|
1664
|
+
continue;
|
|
1665
|
+
try {
|
|
1666
|
+
const ref = JSON.parse(part.state.input.prompt.slice("SORTIE_OPERATOR_TASK_REF ".length));
|
|
1667
|
+
if (isRecord(ref) && ref.r === root && ref.n === runID && ref.t === unitID &&
|
|
1668
|
+
/^[a-f0-9]{64}$/u.test(String(ref.p)) && /^[a-f0-9]{64}$/u.test(String(ref.h)) &&
|
|
1669
|
+
goalFingerprint({ goal_id: goalID, unit_id: unitID, call_id: part.callID }) === reservationID) {
|
|
1670
|
+
const error = isRecord(part.state.error) ? part.state.error : undefined;
|
|
1671
|
+
candidates.push({ callID: part.callID, depthRefused: error?.type === "tool.execution" &&
|
|
1672
|
+
error.message === 'Subagent depth limit reached (1). Increase "experimental.subagent_depth" to allow nested subagents.' });
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
catch { /* malformed native reference cannot prove this reservation */ }
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (candidates.length !== 1)
|
|
1679
|
+
return undefined;
|
|
1680
|
+
if (childrenMethod !== undefined) {
|
|
1681
|
+
const descendantsResponse = await childrenMethod.call(sessionAPI, { path: { id: coordinator }, query: { directory: input.directory } });
|
|
1682
|
+
if (!isRecord(descendantsResponse) || !Array.isArray(descendantsResponse.data) || descendantsResponse.data.length !== 0)
|
|
1683
|
+
return undefined;
|
|
1684
|
+
}
|
|
1685
|
+
else if (mission.submission?.status !== "blocked" || !candidates[0].depthRefused)
|
|
1686
|
+
return undefined;
|
|
1687
|
+
return candidates[0].callID;
|
|
1688
|
+
}
|
|
1612
1689
|
for (const reservation of pending) {
|
|
1613
1690
|
const matches = [];
|
|
1614
1691
|
for (const message of data) {
|
|
@@ -1686,8 +1763,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1686
1763
|
matches.push({ callID: part.callID, status: String(part.state.status), elapsed });
|
|
1687
1764
|
}
|
|
1688
1765
|
}
|
|
1689
|
-
if (matches.length !== 1)
|
|
1690
|
-
|
|
1766
|
+
if (matches.length !== 1) {
|
|
1767
|
+
const callID = matches.length === 0 ? await terminalCancelledMissionTask(reservation.unit_id, reservation.reservation_id, state.goal_id) : undefined;
|
|
1768
|
+
if (callID === undefined)
|
|
1769
|
+
continue;
|
|
1770
|
+
matches.push({ callID, status: "error", elapsed: null });
|
|
1771
|
+
}
|
|
1691
1772
|
const match = matches[0];
|
|
1692
1773
|
// Reconcile lifecycle accounting only. Lost in-memory validation bindings cannot be
|
|
1693
1774
|
// reconstructed from worker prose and must not manufacture acceptance evidence.
|
|
@@ -7743,6 +7824,9 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7743
7824
|
currentBudget: async (root) => {
|
|
7744
7825
|
if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
|
|
7745
7826
|
throw new Error("operator-coordinator-required");
|
|
7827
|
+
// Planning must account for terminal native Tasks before reserving new units.
|
|
7828
|
+
// Otherwise a cancelled predecessor keeps a stale unit reservation across missions.
|
|
7829
|
+
await recoverCompletedGoalReservations(root);
|
|
7746
7830
|
const state = await currentGoal(root);
|
|
7747
7831
|
if (state.goal_id === null || state.budget === null)
|
|
7748
7832
|
return null;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* OpenCode V2's package loader resolves the `./plugin` export. Expose its
|
|
3
|
+
* default definition here as well as the named V1 factory used by wrappers.
|
|
3
4
|
*
|
|
4
|
-
* OpenCode configuration lists the package name in the `
|
|
5
|
-
* `"
|
|
5
|
+
* OpenCode configuration lists the package name in the `plugins` array:
|
|
6
|
+
* `"plugins": ["sortie-dogs"]`. V1 wrappers can still use
|
|
6
7
|
* `import { SortieDogsPlugin } from "sortie-dogs/plugin"`.
|
|
7
8
|
*
|
|
8
|
-
* OpenCode
|
|
9
|
-
*
|
|
10
|
-
* load and silently disables the plugin. This entry exposes the factory and nothing else; every
|
|
11
|
-
* other runtime symbol stays on the package root.
|
|
9
|
+
* OpenCode V2 requires a default definition with an id and setup or effect function.
|
|
10
|
+
* Keep other runtime symbols on the package root rather than this shared entry.
|
|
12
11
|
*/
|
|
13
12
|
export { SortieDogsV010Plugin as SortieDogsPlugin } from "./profiled.js";
|
|
13
|
+
export { default } from "./v2.js";
|
|
14
14
|
export type { OpenCodeEvent, OpenCodeHooks, OpenCodePlugin, OpenCodePluginInput, SortieDogsPluginOptions, } from "./index.js";
|
package/dist/plugin/opencode.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* OpenCode V2's package loader resolves the `./plugin` export. Expose its
|
|
3
|
+
* default definition here as well as the named V1 factory used by wrappers.
|
|
3
4
|
*
|
|
4
|
-
* OpenCode configuration lists the package name in the `
|
|
5
|
-
* `"
|
|
5
|
+
* OpenCode configuration lists the package name in the `plugins` array:
|
|
6
|
+
* `"plugins": ["sortie-dogs"]`. V1 wrappers can still use
|
|
6
7
|
* `import { SortieDogsPlugin } from "sortie-dogs/plugin"`.
|
|
7
8
|
*
|
|
8
|
-
* OpenCode
|
|
9
|
-
*
|
|
10
|
-
* load and silently disables the plugin. This entry exposes the factory and nothing else; every
|
|
11
|
-
* other runtime symbol stays on the package root.
|
|
9
|
+
* OpenCode V2 requires a default definition with an id and setup or effect function.
|
|
10
|
+
* Keep other runtime symbols on the package root rather than this shared entry.
|
|
12
11
|
*/
|
|
13
12
|
export { SortieDogsV010Plugin as SortieDogsPlugin } from "./profiled.js";
|
|
13
|
+
export { default } from "./v2.js";
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -376,6 +376,35 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
376
376
|
throw new Error("mission-controller-required");
|
|
377
377
|
return { root, mission };
|
|
378
378
|
}
|
|
379
|
+
async function reconcileMissionDispatch(root) {
|
|
380
|
+
const mission = await missions.read(root);
|
|
381
|
+
if (!mission?.dispatchOpen || !mission.callID || ["cancelled", "completed"].includes(mission.phase) ||
|
|
382
|
+
taskOwners.has(mission.callID))
|
|
383
|
+
return mission;
|
|
384
|
+
const history = await messages(root);
|
|
385
|
+
const finished = history.flatMap(message => Array.isArray(message.parts) ? message.parts : []).filter(part => record(part) && part.type === "tool" && part.tool === "task" && part.callID === mission.callID &&
|
|
386
|
+
record(part.state) && ["completed", "error"].includes(String(part.state.status)) &&
|
|
387
|
+
record(part.state.input) && part.state.input.subagent_type === profileAgent(profile, "dog-operator") &&
|
|
388
|
+
(part.state.input.task_id === undefined || part.state.input.task_id === mission.coordinator) &&
|
|
389
|
+
(mission.coordinator !== null || (part.state.input.prompt === missions.task(mission).prompt &&
|
|
390
|
+
part.state.input.task_id === undefined)));
|
|
391
|
+
if (finished.length !== 1)
|
|
392
|
+
return mission;
|
|
393
|
+
if (mission.coordinator !== null) {
|
|
394
|
+
const who = await identity(mission.coordinator);
|
|
395
|
+
if (who.parent !== root || who.role !== "dog-operator")
|
|
396
|
+
return mission;
|
|
397
|
+
}
|
|
398
|
+
return missions.reconcileFinishedDispatch(root, mission.id, mission.callID);
|
|
399
|
+
}
|
|
400
|
+
function missionDispatchPacket(mission, run) {
|
|
401
|
+
const packet = missionPacket(mission, run);
|
|
402
|
+
if (!mission.dispatchOpen && ["open", "running"].includes(mission.phase)) {
|
|
403
|
+
return { ...packet, task: missions.task(mission),
|
|
404
|
+
next_action: "The previous Coordinator Task is finished. Dispatch this exact Task to continue the same mission and Coordinator session; keep the original requirements and cumulative budget." };
|
|
405
|
+
}
|
|
406
|
+
return packet;
|
|
407
|
+
}
|
|
379
408
|
async function restorePriorAcceptance(root, state) {
|
|
380
409
|
const succeeded = [...state.units].reverse().find(unit => unit.status === "succeeded");
|
|
381
410
|
const current = succeeded === undefined ? undefined : {
|
|
@@ -608,12 +637,17 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
608
637
|
const root = await rootFor(context.sessionID);
|
|
609
638
|
if (!root)
|
|
610
639
|
throw new Error("operator-grant-invalid");
|
|
640
|
+
const activeMission = await reconcileMissionDispatch(root);
|
|
641
|
+
if (activeMission && context.sessionID === root && !activeMission.dispatchOpen &&
|
|
642
|
+
["open", "running"].includes(activeMission.phase)) {
|
|
643
|
+
return JSON.stringify({ ...missionDispatchPacket(activeMission, await operators.read(root)), budget: await control.currentBudget(root) });
|
|
644
|
+
}
|
|
611
645
|
const result = await operators.next(root, context.sessionID);
|
|
612
646
|
const mission = await missions.read(root);
|
|
613
647
|
if (mission && !["cancelled", "completed"].includes(mission.phase)) {
|
|
614
648
|
if (record(result) && record(result.task))
|
|
615
649
|
return JSON.stringify(result);
|
|
616
|
-
return JSON.stringify({ ...
|
|
650
|
+
return JSON.stringify({ ...missionDispatchPacket(mission, await operators.required(root)), budget: await control.currentBudget(root) });
|
|
617
651
|
}
|
|
618
652
|
if (context.sessionID === root && record(result) && record(result.task)) {
|
|
619
653
|
const state = await operators.required(root);
|
|
@@ -652,9 +686,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
652
686
|
tools[status] = { description: "Read the durable root-owned operator outcome and host budget counters (max_units, consumed_units, reserved_units, remaining_units) without claiming acceptance or retrying work. An investigating proposal returns its exact short Task reference only before a Task has been admitted; an existing admission never yields a redispatch Task.",
|
|
653
687
|
args: {}, execute: async (_args, context) => {
|
|
654
688
|
const root = await rootFor(context.sessionID);
|
|
655
|
-
const mission = root ? await missions.read(root) : undefined;
|
|
689
|
+
const mission = root && context.sessionID === root ? await reconcileMissionDispatch(root) : root ? await missions.read(root) : undefined;
|
|
656
690
|
if (mission && (context.sessionID === root || context.sessionID === mission.coordinator)) {
|
|
657
|
-
return JSON.stringify({ ...
|
|
691
|
+
return JSON.stringify({ ...missionDispatchPacket(mission, await operators.read(root)), budget: await control.currentBudget(root) });
|
|
658
692
|
}
|
|
659
693
|
await requireRoot(context.sessionID);
|
|
660
694
|
const state = await operators.read(context.sessionID);
|
|
@@ -1083,9 +1117,12 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1083
1117
|
tools[startMission] = { description: "Operator: save a few one-line requirements/negative constraints. The original user message, IDs and ownership are captured automatically. Dispatch the returned Coordinator task immediately, or use plan_units for a simple single-unit Fast-lane task.",
|
|
1084
1118
|
args: { requirements: { ...stringList, minItems: 1, maxItems: 64 } }, execute: async (args, context) => {
|
|
1085
1119
|
await requireRoot(context.sessionID);
|
|
1120
|
+
await reconcileMissionDispatch(context.sessionID);
|
|
1086
1121
|
const mission = await missions.start(context.sessionID, args.requirements);
|
|
1087
|
-
return JSON.stringify(
|
|
1088
|
-
|
|
1122
|
+
return JSON.stringify(mission.dispatchOpen
|
|
1123
|
+
? missionDispatchPacket(mission, await operators.read(context.sessionID))
|
|
1124
|
+
: { mission_id: mission.id, requirements: mission.requirements, task: missions.task(mission),
|
|
1125
|
+
next_action: "Nontrivial: dispatch task now. Simple single-unit work with known scope/check: call plan_units directly. Do not create a proposal or ask for plan approval." });
|
|
1089
1126
|
} };
|
|
1090
1127
|
async function declareMissionUnits(root, actor, mission, raw, reason) {
|
|
1091
1128
|
const plan = missionPlan(mission, raw);
|
|
@@ -1102,7 +1139,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1102
1139
|
if (budget && budget.remaining_units < plan.units.length && !same)
|
|
1103
1140
|
throw new Error("mission-budget-exhausted: report the required cumulative extension to Operator");
|
|
1104
1141
|
const state = await operators.prepareMission(root, plan, actor === root ? undefined
|
|
1105
|
-
: { sessionID: actor, callID: mission.callID });
|
|
1142
|
+
: { sessionID: actor, callID: mission.callID }, mission.supersededRunID);
|
|
1106
1143
|
await restorePriorAcceptance(root, state);
|
|
1107
1144
|
await control.registerGoalDeclaration(root, state.units[0].task.prompt, true);
|
|
1108
1145
|
control.enableUnits(root, state.units.filter(unit => unit.status === "pending").length);
|
|
@@ -1112,6 +1149,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1112
1149
|
item.runID = state.runID;
|
|
1113
1150
|
item.phase = "running";
|
|
1114
1151
|
item.submission = null;
|
|
1152
|
+
delete item.supersededRunID;
|
|
1115
1153
|
});
|
|
1116
1154
|
return JSON.stringify(await operators.next(root, actor));
|
|
1117
1155
|
}
|
|
@@ -1498,6 +1536,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1498
1536
|
if (request.tool === "task" && args.subagent_type === profileAgent(profile, "dog-operator") &&
|
|
1499
1537
|
mission && !["completed", "cancelled"].includes(mission.phase)) {
|
|
1500
1538
|
await requireRoot(request.sessionID);
|
|
1539
|
+
await reconcileMissionDispatch(root);
|
|
1501
1540
|
await missions.admit(root, request.callID, args);
|
|
1502
1541
|
taskOwners.set(request.callID, { root, actor: request.sessionID, operator: false, mission: true });
|
|
1503
1542
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sortie-dogs",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"ajv": "^8.17.1",
|
|
75
|
-
"ajv-formats": "^3.0.1"
|
|
75
|
+
"ajv-formats": "^3.0.1",
|
|
76
|
+
"jsonc-parser": "^3.3.1"
|
|
76
77
|
},
|
|
77
78
|
"devDependencies": {
|
|
78
79
|
"@types/node": "^22.0.0",
|