sortie-dogs 0.12.6 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/main.js +8 -0
- package/dist/core/initialize.d.ts +9 -0
- package/dist/core/initialize.js +28 -0
- package/dist/core/operator-mission.d.ts +14 -1
- package/dist/core/operator-mission.js +50 -9
- package/dist/core/operator-runtime.d.ts +4 -2
- package/dist/core/operator-runtime.js +41 -3
- package/dist/plugin/index.js +11 -2
- package/dist/plugin/mission-review.js +3 -1
- package/dist/plugin/profiled.d.ts +8 -0
- package/dist/plugin/profiled.js +96 -16
- package/dist/plugin/runtime-bridge.d.ts +1 -0
- package/dist/plugin/v2.js +3 -2
- package/dist/runtime-assets-v010.js +9 -2
- package/dist/runtime-mission-assets.d.ts +2 -0
- package/dist/runtime-mission-assets.js +21 -6
- package/package.json +1 -1
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.12.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.12.8-mission-lineage-v1";
|
|
7
7
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.12.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.12.8-mission-lineage-v1";
|
package/dist/cli/main.js
CHANGED
|
@@ -258,6 +258,14 @@ export async function run(argv) {
|
|
|
258
258
|
if (initialized.preservedLegacyPaths.length > 0) {
|
|
259
259
|
process.stdout.write(`Preserved legacy runtime files: ${initialized.preservedLegacyPaths.join(", ")}.\n`);
|
|
260
260
|
}
|
|
261
|
+
const override = profile === "v010"
|
|
262
|
+
? await initializer.coordinatorModelOverride(global ? target : resolve(initArgs[0] ?? process.cwd()), global).catch(() => undefined)
|
|
263
|
+
: undefined;
|
|
264
|
+
if (override) {
|
|
265
|
+
process.stderr.write(`Warning: ${override.path} routes dogs-coordinator to ${override.model}. ` +
|
|
266
|
+
`Sortie-dogs expects ${initializer.V010_COORDINATOR_MODEL}; a gpt-6-luna-fast Coordinator was measured to repeat plans and reviews until timeout. ` +
|
|
267
|
+
"Remove the override unless it is intentional.\n");
|
|
268
|
+
}
|
|
261
269
|
return 0;
|
|
262
270
|
}
|
|
263
271
|
catch (error) {
|
|
@@ -11,6 +11,15 @@ export declare class ProjectInitializationError extends Error {
|
|
|
11
11
|
readonly code: ProjectInitializationErrorCode;
|
|
12
12
|
constructor(code: ProjectInitializationErrorCode, message: string, options?: ErrorOptions);
|
|
13
13
|
}
|
|
14
|
+
export declare const V010_COORDINATOR_MODEL = "openai/gpt-6-sol#xhigh";
|
|
15
|
+
/**
|
|
16
|
+
* Report a user config that routes the v0.10 Coordinator away from its packaged model. Measured Luna
|
|
17
|
+
* Coordinators repeated plans and review dispatches until timeout, so init surfaces it without editing it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function coordinatorModelOverride(root: string, global: boolean): Promise<{
|
|
20
|
+
path: string;
|
|
21
|
+
model: string;
|
|
22
|
+
} | undefined>;
|
|
14
23
|
/** Resolves the OpenCode global configuration directory without platform-specific paths. */
|
|
15
24
|
export declare function resolveGlobalConfigRoot(env?: NodeJS.ProcessEnv, home?: string): Promise<string>;
|
|
16
25
|
export declare function initializeProject(projectRoot?: string, profile?: RuntimeProfileId): Promise<InitializeProjectResult>;
|
package/dist/core/initialize.js
CHANGED
|
@@ -536,6 +536,34 @@ async function initializeRoot(requestedRoot, layout, installAssets = runtimeAsse
|
|
|
536
536
|
preservedLegacyPaths,
|
|
537
537
|
};
|
|
538
538
|
}
|
|
539
|
+
export const V010_COORDINATOR_MODEL = "openai/gpt-6-sol#xhigh";
|
|
540
|
+
/**
|
|
541
|
+
* Report a user config that routes the v0.10 Coordinator away from its packaged model. Measured Luna
|
|
542
|
+
* Coordinators repeated plans and review dispatches until timeout, so init surfaces it without editing it.
|
|
543
|
+
*/
|
|
544
|
+
export async function coordinatorModelOverride(root, global) {
|
|
545
|
+
const prefix = global ? "" : `${OPEN_CODE_DIRECTORY}/`;
|
|
546
|
+
for (const relativePath of [`${prefix}opencode.jsonc`, `${prefix}opencode.json`]) {
|
|
547
|
+
let content;
|
|
548
|
+
try {
|
|
549
|
+
content = await readFile(resolve(root, relativePath), "utf8");
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (error.code === "ENOENT")
|
|
553
|
+
continue;
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
const config = parse(content, [], { allowTrailingComma: true });
|
|
557
|
+
for (const key of ["agents", "agent"]) {
|
|
558
|
+
const agents = config?.[key];
|
|
559
|
+
const coordinator = agents !== null && typeof agents === "object" ? agents["dogs-coordinator"] : undefined;
|
|
560
|
+
const model = coordinator !== null && typeof coordinator === "object" ? coordinator.model : undefined;
|
|
561
|
+
if (typeof model === "string" && model !== V010_COORDINATOR_MODEL)
|
|
562
|
+
return { path: resolve(root, relativePath), model };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
539
567
|
/** Resolves the OpenCode global configuration directory without platform-specific paths. */
|
|
540
568
|
export async function resolveGlobalConfigRoot(env = process.env, home = homedir()) {
|
|
541
569
|
if (env.OPENCODE_CONFIG_DIR)
|
|
@@ -40,11 +40,22 @@ export interface OperatorMission {
|
|
|
40
40
|
risk: string[];
|
|
41
41
|
source: string;
|
|
42
42
|
task: OperatorTask | null;
|
|
43
|
-
verdict: "pending" | "PASS" | "findings" | "skipped-low-risk";
|
|
43
|
+
verdict: "pending" | "PASS" | "findings" | "evidence-gaps" | "skipped-low-risk";
|
|
44
44
|
result?: string;
|
|
45
45
|
child?: string;
|
|
46
|
+
/** Reviews on this mission that found only missing evidence; bounded by MISSION_EVIDENCE_GAP_REVIEW_LIMIT. */
|
|
47
|
+
evidenceGapReviews?: number;
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Evidence-only findings do not establish a defect. Each extra round costs a full Reviewer pass and often a
|
|
52
|
+
* Worker unit, so after this many evidence-only reviews the candidate may be submitted with the gaps listed.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
|
|
55
|
+
/** Classify an independent Reviewer's first line. Anything else is a finding. */
|
|
56
|
+
export declare function missionReviewVerdict(text: string): "PASS" | "evidence-gaps" | "findings";
|
|
57
|
+
/** Whether the recorded review permits submission and acceptance of the current candidate. */
|
|
58
|
+
export declare function missionReviewAccepted(review: NonNullable<OperatorMission["review"]>): boolean;
|
|
48
59
|
/** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
|
|
49
60
|
export declare class OperatorMissionRuntime {
|
|
50
61
|
readonly projectRoot: string;
|
|
@@ -64,6 +75,8 @@ export declare class OperatorMissionRuntime {
|
|
|
64
75
|
capture(root: string, request: MissionRequest): Promise<void>;
|
|
65
76
|
start(root: string, requirements: unknown): Promise<OperatorMission>;
|
|
66
77
|
update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
|
|
78
|
+
/** Keep a cancelled same-turn run's host-accepted criteria ahead of new mission text. */
|
|
79
|
+
carryForward(root: string, missionID: string, acceptance: readonly string[]): Promise<OperatorMission>;
|
|
67
80
|
task(state: OperatorMission): OperatorTask;
|
|
68
81
|
admit(root: string, callID: string, args: Record<string, unknown>): Promise<OperatorMission>;
|
|
69
82
|
/** Reopen only the exact dispatch whose native Task has a terminal record after a server restart. */
|
|
@@ -18,6 +18,20 @@ export function missionValidationCommand(command) {
|
|
|
18
18
|
const expression = `exec(${JSON.stringify(heredoc[4])})`;
|
|
19
19
|
return `${heredoc[1]} -c '${expression.replaceAll("'", "'\\''")}'`;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Evidence-only findings do not establish a defect. Each extra round costs a full Reviewer pass and often a
|
|
23
|
+
* Worker unit, so after this many evidence-only reviews the candidate may be submitted with the gaps listed.
|
|
24
|
+
*/
|
|
25
|
+
export const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
|
|
26
|
+
/** Classify an independent Reviewer's first line. Anything else is a finding. */
|
|
27
|
+
export function missionReviewVerdict(text) {
|
|
28
|
+
return /^\s*PASS(?:\s|$)/u.test(text) ? "PASS" : /^\s*EVIDENCE_GAPS(?:\s|$)/u.test(text) ? "evidence-gaps" : "findings";
|
|
29
|
+
}
|
|
30
|
+
/** Whether the recorded review permits submission and acceptance of the current candidate. */
|
|
31
|
+
export function missionReviewAccepted(review) {
|
|
32
|
+
return review.verdict === "PASS" || review.verdict === "skipped-low-risk" ||
|
|
33
|
+
(review.verdict === "evidence-gaps" && (review.evidenceGapReviews ?? 0) >= MISSION_EVIDENCE_GAP_REVIEW_LIMIT);
|
|
34
|
+
}
|
|
21
35
|
/** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
|
|
22
36
|
export class OperatorMissionRuntime {
|
|
23
37
|
projectRoot;
|
|
@@ -44,7 +58,7 @@ export class OperatorMissionRuntime {
|
|
|
44
58
|
async loadMission(root) {
|
|
45
59
|
const state = await this.load(this.file(root));
|
|
46
60
|
if (!state || state.supersededRunID !== undefined || state.runID !== null ||
|
|
47
|
-
!["open", "running", "submitted"].includes(state.phase) || state.requests.length === 0)
|
|
61
|
+
!["open", "running", "submitted", "cancelled"].includes(state.phase) || state.requests.length === 0)
|
|
48
62
|
return state;
|
|
49
63
|
const directory = join(this.projectRoot, this.profile.stateDirectory, "missions");
|
|
50
64
|
let entries;
|
|
@@ -61,13 +75,18 @@ export class OperatorMissionRuntime {
|
|
|
61
75
|
const matches = [];
|
|
62
76
|
for (const name of candidates) {
|
|
63
77
|
const previous = await this.load(join(directory, name));
|
|
64
|
-
if (previous?.root === root && previous.phase === "cancelled" &&
|
|
65
|
-
previous.
|
|
78
|
+
if (previous?.root === root && previous.phase === "cancelled" &&
|
|
79
|
+
(previous.runID !== null || previous.supersededRunID !== undefined) &&
|
|
80
|
+
(previous.runID === null || previous.requests[0]?.id !== state.requests[0].id) &&
|
|
66
81
|
previous.requests.some(request => request.id === state.requests[0].id))
|
|
67
82
|
matches.push(previous);
|
|
68
83
|
}
|
|
69
|
-
if (matches.length === 1)
|
|
70
|
-
|
|
84
|
+
if (matches.length === 1) {
|
|
85
|
+
const predecessor = matches[0].runID ?? matches[0].supersededRunID;
|
|
86
|
+
if (predecessor && (matches[0].runID !== null || await this.archivedRun(root, predecessor))) {
|
|
87
|
+
state.supersededRunID = predecessor;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
71
90
|
return state;
|
|
72
91
|
}
|
|
73
92
|
/** Find exactly one archived cancelled mission that owned this run; ambiguity never grants recovery. */
|
|
@@ -164,8 +183,10 @@ export class OperatorMissionRuntime {
|
|
|
164
183
|
const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
|
|
165
184
|
requirements: requirements.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "open",
|
|
166
185
|
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
|
|
167
|
-
...(previous?.phase === "cancelled" &&
|
|
168
|
-
|
|
186
|
+
...(previous?.phase === "cancelled" &&
|
|
187
|
+
(previous.runID === null || request.id !== previous.requests[0]?.id) &&
|
|
188
|
+
(previous.runID ?? previous.supersededRunID)
|
|
189
|
+
? { supersededRunID: (previous.runID ?? previous.supersededRunID) } : {}) };
|
|
169
190
|
await this.save(this.file(root), state);
|
|
170
191
|
return state;
|
|
171
192
|
});
|
|
@@ -180,6 +201,23 @@ export class OperatorMissionRuntime {
|
|
|
180
201
|
return state;
|
|
181
202
|
});
|
|
182
203
|
}
|
|
204
|
+
/** Keep a cancelled same-turn run's host-accepted criteria ahead of new mission text. */
|
|
205
|
+
carryForward(root, missionID, acceptance) {
|
|
206
|
+
return this.update(root, state => {
|
|
207
|
+
if (state.id !== missionID || state.supersededRunID !== undefined || state.runID !== null ||
|
|
208
|
+
!["open", "running"].includes(state.phase) || (state.dispatchOpen && state.coordinator === null) ||
|
|
209
|
+
acceptance.length === 0 || acceptance.some(text => typeof text !== "string" || !text.trim())) {
|
|
210
|
+
throw new Error("mission-acceptance-carry-forward-unavailable");
|
|
211
|
+
}
|
|
212
|
+
const existing = state.requirements.map(item => item.text);
|
|
213
|
+
if (acceptance.every((text, index) => existing[index] === text))
|
|
214
|
+
return;
|
|
215
|
+
const additions = existing.filter(text => !acceptance.includes(text));
|
|
216
|
+
if (acceptance.length + additions.length > 64)
|
|
217
|
+
throw new Error("mission-requirements-limit: carried acceptance and additions exceed 64");
|
|
218
|
+
state.requirements = [...acceptance, ...additions].map((text, index) => ({ id: `R${index + 1}`, text }));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
183
221
|
task(state) {
|
|
184
222
|
return { subagent_type: profileAgent(this.profile, "dog-operator"), description: state.requirements[0].text.slice(0, 100),
|
|
185
223
|
prompt: `${MISSION_REFERENCE}${JSON.stringify({ r: state.root, m: state.id, h: digest(JSON.stringify(state.requirements)) })}`,
|
|
@@ -294,12 +332,15 @@ export function missionPacket(mission, run) {
|
|
|
294
332
|
submission: mission.submission, progress: mission.progress,
|
|
295
333
|
review: mission.review ? { risk_tags: mission.review.risk, verdict: mission.review.verdict,
|
|
296
334
|
source_fingerprint: mission.review.source, reviewer_session_id: mission.review.child ?? null,
|
|
297
|
-
result: mission.review.result ?? null
|
|
335
|
+
result: mission.review.result ?? null, evidence_gap_reviews: mission.review.evidenceGapReviews ?? 0,
|
|
336
|
+
evidence_gap_review_limit: MISSION_EVIDENCE_GAP_REVIEW_LIMIT, accepted: missionReviewAccepted(mission.review) } : null,
|
|
298
337
|
...(run ? { run_id: run.runID, status: run.phase, decision: run.decision,
|
|
299
338
|
units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
|
|
300
339
|
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence })) } : {}),
|
|
301
340
|
next_action: run?.phase === "awaiting-decision" ? "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative."
|
|
302
|
-
: run?.phase === "awaiting-acceptance" ?
|
|
341
|
+
: run?.phase === "awaiting-acceptance" ? (mission.review?.verdict === "evidence-gaps" && !missionReviewAccepted(mission.review)
|
|
342
|
+
? "Coordinator: the Reviewer found only missing evidence. Supply it through review_mission traces (or at most one evidence-only unit) and re-review once; do not re-implement. At the evidence-gap limit, submit_mission ready with the gaps listed."
|
|
343
|
+
: "Coordinator: obtain required independent review, then submit_mission. Operator: compare all requirements with source/evidence before complete_mission.")
|
|
303
344
|
: "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
|
|
304
345
|
}
|
|
305
346
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
@@ -109,7 +109,7 @@ interface UnitState {
|
|
|
109
109
|
readonly task: OperatorTask;
|
|
110
110
|
readonly handoffPath: string;
|
|
111
111
|
readonly manifestPath: string;
|
|
112
|
-
|
|
112
|
+
hashes: readonly string[];
|
|
113
113
|
status: "pending" | "running" | "succeeded" | "failed" | "cancelled";
|
|
114
114
|
callID: string | null;
|
|
115
115
|
childSessionID: string | null;
|
|
@@ -245,7 +245,7 @@ export declare class OperatorRuntime {
|
|
|
245
245
|
prepareMission(root: string, raw: unknown, dispatcher?: {
|
|
246
246
|
sessionID: string;
|
|
247
247
|
callID: string;
|
|
248
|
-
}, supersededRunID?: string): Promise<OperatorState>;
|
|
248
|
+
}, supersededRunID?: string, terminalChildren?: readonly string[]): Promise<OperatorState>;
|
|
249
249
|
retireMissionRun(root: string): Promise<void>;
|
|
250
250
|
private prepareOnce;
|
|
251
251
|
operatorTask(state: OperatorState): OperatorTask;
|
|
@@ -332,6 +332,8 @@ export declare class OperatorRuntime {
|
|
|
332
332
|
packet(state: OperatorState): unknown;
|
|
333
333
|
continuationCheckpoint(root: string): Promise<string | undefined>;
|
|
334
334
|
private verifyControls;
|
|
335
|
+
/** Follow only the host's authorized parent-link repair; keep the durable control pin in sync. */
|
|
336
|
+
acknowledgeHostHandoffRepair(root: string, taskID: string, path: string, original: string, repaired: string): Promise<void>;
|
|
335
337
|
private gitPaths;
|
|
336
338
|
private pathAuthorized;
|
|
337
339
|
/**
|
|
@@ -678,8 +678,8 @@ export class OperatorRuntime {
|
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
/** Mission authority is supplied only by the owning profile, never by a model-authored plan. */
|
|
681
|
-
prepareMission(root, raw, dispatcher, supersededRunID) {
|
|
682
|
-
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID }));
|
|
681
|
+
prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = []) {
|
|
682
|
+
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren }));
|
|
683
683
|
}
|
|
684
684
|
retireMissionRun(root) {
|
|
685
685
|
return this.serial(root, async () => {
|
|
@@ -727,9 +727,15 @@ export class OperatorRuntime {
|
|
|
727
727
|
previous.phase === "cancelled";
|
|
728
728
|
if (mission?.supersededRunID !== undefined && !superseding)
|
|
729
729
|
throw new Error("mission-superseded-run-mismatch");
|
|
730
|
+
const terminalChildren = mission?.terminalChildren ?? [];
|
|
731
|
+
const cancelledChildren = previous?.units.flatMap(unit => unit.status === "cancelled" && unit.childSessionID !== null
|
|
732
|
+
? [unit.childSessionID] : []) ?? [];
|
|
730
733
|
if (superseding && previous && (previous.decision !== "explicit-cancellation" || previous.gitLifecycle !== null ||
|
|
731
734
|
previous.repairResidualPaths.length > 0 || previous.priorAcceptedUnits.length > 0 ||
|
|
732
|
-
|
|
735
|
+
terminalChildren.length !== cancelledChildren.length || new Set(terminalChildren).size !== terminalChildren.length ||
|
|
736
|
+
cancelledChildren.some(id => !terminalChildren.includes(id)) || previous.units.some(unit => !["pending", "cancelled"].includes(unit.status) || unit.evidence.length > 0 || unit.resultClass !== null ||
|
|
737
|
+
unit.repairValidation !== null || (unit.status === "pending" && (unit.callID !== null || unit.childSessionID !== null)) ||
|
|
738
|
+
(unit.status === "cancelled" && (unit.childSessionID === null || unit.callID === null))))) {
|
|
733
739
|
throw new Error("mission-superseded-run-has-work: reconcile prior workers and evidence before changing acceptance");
|
|
734
740
|
}
|
|
735
741
|
if (plan.git_lifecycle !== undefined) {
|
|
@@ -743,6 +749,14 @@ export class OperatorRuntime {
|
|
|
743
749
|
// Cancellation stops execution, not the accepted user order. A replacement
|
|
744
750
|
// plan must carry the original ordered criteria; only a completed root clears them.
|
|
745
751
|
const parent = !superseding && previous?.phase === "cancelled" ? previous : undefined;
|
|
752
|
+
if (parent?.decision === "explicit-cancellation") {
|
|
753
|
+
const cancelled = parent.units.flatMap(unit => unit.status === "cancelled" && unit.childSessionID !== null
|
|
754
|
+
? [unit.childSessionID] : []);
|
|
755
|
+
if (cancelled.length > 0 && (new Set(terminalChildren).size !== terminalChildren.length ||
|
|
756
|
+
cancelled.some(id => !terminalChildren.includes(id)))) {
|
|
757
|
+
throw new Error("mission-cancelled-run-worker-not-terminal");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
746
760
|
if (parent?.decision === "operator-contract-repair-unavailable-after-cancel") {
|
|
747
761
|
// The diagnosed transients, not the cancellation itself, block a replacement. Once they are
|
|
748
762
|
// gone the same accepted order may proceed; while they remain the refusal names them exactly.
|
|
@@ -1893,6 +1907,30 @@ export class OperatorRuntime {
|
|
|
1893
1907
|
if (contents.some((value, index) => hash(value) !== unit.hashes[index]))
|
|
1894
1908
|
throw new Error("operator-contract-changed");
|
|
1895
1909
|
}
|
|
1910
|
+
/** Follow only the host's authorized parent-link repair; keep the durable control pin in sync. */
|
|
1911
|
+
acknowledgeHostHandoffRepair(root, taskID, path, original, repaired) {
|
|
1912
|
+
return this.serial(root, async () => {
|
|
1913
|
+
const state = await this.read(root);
|
|
1914
|
+
const unit = state?.units.find(candidate => candidate.status === "running" && candidate.handoffPath === path &&
|
|
1915
|
+
/^task_id: (.+)$/m.exec(candidate.task.prompt)?.[1] === taskID);
|
|
1916
|
+
if (!unit)
|
|
1917
|
+
return; // The same host repair also serves non-Operator handoffs.
|
|
1918
|
+
if (hash(original) !== unit.hashes[0] || await readFile(path, "utf8") !== repaired) {
|
|
1919
|
+
throw new Error("operator-host-handoff-repair-unpinned");
|
|
1920
|
+
}
|
|
1921
|
+
const before = JSON.parse(original), after = JSON.parse(repaired);
|
|
1922
|
+
const oldLink = inspectAcceptanceContinuity(before).ledger;
|
|
1923
|
+
const newLink = inspectAcceptanceContinuity(after).ledger;
|
|
1924
|
+
if (!oldLink || !newLink || oldLink.task_id !== taskID || oldLink.parent_fingerprint !== "none" ||
|
|
1925
|
+
newLink.task_id !== taskID || newLink.fingerprint !== oldLink.fingerprint ||
|
|
1926
|
+
newLink.parent_fingerprint === "none" ||
|
|
1927
|
+
JSON.stringify({ ...before, ext: { ...before.ext, [ACCEPTANCE_CONTINUITY_EXTENSION]: { ...oldLink, parent_fingerprint: newLink.parent_fingerprint } } }, null, 2) + "\n" !== repaired) {
|
|
1928
|
+
throw new Error("operator-host-handoff-repair-invalid");
|
|
1929
|
+
}
|
|
1930
|
+
unit.hashes = [hash(repaired), ...unit.hashes.slice(1)];
|
|
1931
|
+
await this.save(state);
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1896
1934
|
gitPaths(output) {
|
|
1897
1935
|
return output.split("\0").filter(Boolean).map(path => normalizeRelativePath(path.replaceAll("\\", "/")));
|
|
1898
1936
|
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -6953,7 +6953,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6953
6953
|
// Persist it so the worker's Read and later recovery see the same contract.
|
|
6954
6954
|
if (reservedParallelDescriptor === undefined && ledger.parent_fingerprint === "none" &&
|
|
6955
6955
|
(exactCarryForward || strictAppend)) {
|
|
6956
|
-
const
|
|
6956
|
+
const source = await readFile(handoffPaths[0], "utf8");
|
|
6957
|
+
const handoff = JSON.parse(source);
|
|
6957
6958
|
const validated = validateHandoffSchema(handoff);
|
|
6958
6959
|
const current = validated.ok ? inspectAcceptanceContinuity(validated.value).ledger : undefined;
|
|
6959
6960
|
if (current === undefined || JSON.stringify(current) !== JSON.stringify(ledger)) {
|
|
@@ -6963,7 +6964,15 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6963
6964
|
}
|
|
6964
6965
|
ledger = { ...ledger, parent_fingerprint: previous.fingerprint };
|
|
6965
6966
|
handoff.ext[ACCEPTANCE_CONTINUITY_EXTENSION] = ledger;
|
|
6966
|
-
|
|
6967
|
+
const repaired = `${JSON.stringify(handoff, null, 2)}\n`;
|
|
6968
|
+
await writeFile(handoffPaths[0], repaired);
|
|
6969
|
+
try {
|
|
6970
|
+
await input.runtimeBridge?.onHostHandoffRepaired?.(toolInput.sessionID, ledger.task_id, handoffPaths[0], source, repaired);
|
|
6971
|
+
}
|
|
6972
|
+
catch (error) {
|
|
6973
|
+
await writeFile(handoffPaths[0], source);
|
|
6974
|
+
throw error;
|
|
6975
|
+
}
|
|
6967
6976
|
await inspect(handoffPaths[0], undefined, { report: true });
|
|
6968
6977
|
}
|
|
6969
6978
|
if (ledger.parent_fingerprint !== previous.fingerprint || (!exactCarryForward && !strictAppend)) {
|
|
@@ -3,10 +3,12 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { lstat, readFile, readlink } from "node:fs/promises";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { TOOL_ENVIRONMENT } from "../runtime-mission-assets.js";
|
|
6
7
|
const exec = promisify(execFile);
|
|
7
8
|
/** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
|
|
8
9
|
export async function missionReviewSource(directory, run) {
|
|
9
|
-
|
|
10
|
+
// The shared dependency environment is local tooling, never reviewed or pinned source.
|
|
11
|
+
const scopes = [...new Set(run.units.flatMap(unit => unit.unit.write)), `:(exclude)${TOOL_ENVIRONMENT}`];
|
|
10
12
|
const git = async (args) => (await exec("git", args, { cwd: directory, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
11
13
|
const names = await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...scopes]);
|
|
12
14
|
const untracked = new Set((await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...scopes])).split("\0").filter(Boolean));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OperatorState } from "../core/operator-runtime.js";
|
|
1
2
|
import { type RuntimeProfile } from "../core/runtime-profile.js";
|
|
2
3
|
import { type OpenCodePlugin } from "./index.js";
|
|
3
4
|
import { type CatalogModel } from "./model-routing.js";
|
|
@@ -20,6 +21,13 @@ export declare function previewModelCatalog(routes?: readonly {
|
|
|
20
21
|
readonly model: string;
|
|
21
22
|
readonly variant: string;
|
|
22
23
|
}[], base?: readonly CatalogModel[]): readonly CatalogModel[];
|
|
24
|
+
/** Native V2 termination and lineage are required before a cancelled Worker loses its old acceptance. */
|
|
25
|
+
export declare function terminalCancelledMissionChildren(profile: RuntimeProfile, root: string, previous: OperatorState, budget: {
|
|
26
|
+
reserved_units: number;
|
|
27
|
+
} | null, host: {
|
|
28
|
+
get(id: string): Promise<unknown>;
|
|
29
|
+
children(id: string): Promise<unknown>;
|
|
30
|
+
}): Promise<string[]>;
|
|
23
31
|
/** One transport namespace around the existing MkII engine; no second write gate or acceptance engine. */
|
|
24
32
|
export declare function createProfiledPlugin(profile: RuntimeProfile, assetVersion: string): OpenCodePlugin;
|
|
25
33
|
export declare const SortieDogsV010Plugin: OpenCodePlugin;
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -12,7 +12,7 @@ import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentati
|
|
|
12
12
|
import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
13
13
|
import { extractWritePaths, normalizeCommand } from "./gate.js";
|
|
14
14
|
import { normalizeRelativePath } from "../core/path.js";
|
|
15
|
-
import { OperatorMissionRuntime, missionPacket, missionPlan, missionReviewTask, missionReviewTraces } from "../core/operator-mission.js";
|
|
15
|
+
import { MISSION_EVIDENCE_GAP_REVIEW_LIMIT, OperatorMissionRuntime, missionPacket, missionPlan, missionReviewAccepted, missionReviewTask, missionReviewTraces, missionReviewVerdict } from "../core/operator-mission.js";
|
|
16
16
|
import { publishMissionProgress } from "./mission-progress.js";
|
|
17
17
|
import { missionReviewSource } from "./mission-review.js";
|
|
18
18
|
import { SOURCE_REVIEW_RISK_TAGS } from "../core/consultation.js";
|
|
@@ -82,6 +82,34 @@ export function previewModelCatalog(routes = PREVIEW_ROUTES, base = BUILT_IN_MOD
|
|
|
82
82
|
}
|
|
83
83
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
84
84
|
const payload = (value) => record(value) && "data" in value ? value.data : value;
|
|
85
|
+
/** Native V2 termination and lineage are required before a cancelled Worker loses its old acceptance. */
|
|
86
|
+
export async function terminalCancelledMissionChildren(profile, root, previous, budget, host) {
|
|
87
|
+
if (!budget || budget.reserved_units !== 0)
|
|
88
|
+
throw new Error("mission-superseded-run-reservations-pending");
|
|
89
|
+
const oldWorkers = previous.units.flatMap(unit => unit.childSessionID === null ? [] : [unit.childSessionID]);
|
|
90
|
+
if (oldWorkers.length === 0)
|
|
91
|
+
return [];
|
|
92
|
+
const coordinatorID = previous.operatorSessionID;
|
|
93
|
+
const coordinator = coordinatorID && await host.get(coordinatorID);
|
|
94
|
+
if (coordinatorID === null || !record(coordinator) || coordinator.parentID !== root ||
|
|
95
|
+
canonicalAgent(profile, coordinator.agent) !== "dog-operator" || coordinator.outcome !== "interrupted") {
|
|
96
|
+
throw new Error("mission-superseded-coordinator-not-terminal");
|
|
97
|
+
}
|
|
98
|
+
const children = await host.children(coordinatorID);
|
|
99
|
+
if (!Array.isArray(children) || children.length !== oldWorkers.length ||
|
|
100
|
+
children.some(child => !record(child) || !oldWorkers.includes(child.id))) {
|
|
101
|
+
throw new Error("mission-superseded-worker-lineage-unproven");
|
|
102
|
+
}
|
|
103
|
+
for (const id of oldWorkers) {
|
|
104
|
+
const worker = await host.get(id), descendants = await host.children(id);
|
|
105
|
+
if (!record(worker) || worker.parentID !== coordinatorID ||
|
|
106
|
+
!["dog-worker", "dog-luna-worker"].includes(canonicalAgent(profile, worker.agent) ?? "") ||
|
|
107
|
+
worker.outcome !== "interrupted" || !Array.isArray(descendants) || descendants.length !== 0) {
|
|
108
|
+
throw new Error("mission-superseded-worker-not-terminal");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return oldWorkers;
|
|
112
|
+
}
|
|
85
113
|
const RUNTIME_PROFILE_SESSION_INACTIVE = "runtime-profile-session-inactive: non-profile agents retain native read, edit, patch, shell, and task tools; continue directly without Sortie profile tools";
|
|
86
114
|
const fallbackOptionalSchema = (schema) => record(schema) && schema.type === "string" && typeof schema.optional !== "function"
|
|
87
115
|
? { ...schema, "x-sortie-optional": true }
|
|
@@ -284,6 +312,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
284
312
|
});
|
|
285
313
|
const runtimeBridge = {
|
|
286
314
|
profile, assetVersion,
|
|
315
|
+
onHostHandoffRepaired: (root, taskID, path, original, repaired) => operators.acknowledgeHostHandoffRepair(root, taskID, path, original, repaired),
|
|
287
316
|
continuationCheckpoint: async (root) => {
|
|
288
317
|
const mission = await missions.read(root);
|
|
289
318
|
if (mission && !["completed", "cancelled"].includes(mission.phase))
|
|
@@ -405,6 +434,19 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
405
434
|
}
|
|
406
435
|
return packet;
|
|
407
436
|
}
|
|
437
|
+
async function retainCancelledMissionAcceptance(root, mission, prior) {
|
|
438
|
+
const previous = prior ?? await operators.read(root);
|
|
439
|
+
if (previous?.phase !== "cancelled" || mission.supersededRunID !== undefined || mission.runID !== null)
|
|
440
|
+
return mission;
|
|
441
|
+
// A different original user request needs the explicit, host-proven supersession path.
|
|
442
|
+
// Only the same source can retain a cancelled run's acceptance as a prefix.
|
|
443
|
+
if (previous.sourceRefs[0] !== `user:${mission.requests[0]?.id}` ||
|
|
444
|
+
!previous.sourceRefs.every(ref => mission.requests.some(request => ref === `user:${request.id}`))) {
|
|
445
|
+
throw new Error("mission-cancelled-source-unproven");
|
|
446
|
+
}
|
|
447
|
+
return previous.acceptance.every((text, index) => mission.requirements[index]?.text === text)
|
|
448
|
+
? mission : missions.carryForward(root, mission.id, previous.acceptance);
|
|
449
|
+
}
|
|
408
450
|
async function restorePriorAcceptance(root, state) {
|
|
409
451
|
const succeeded = [...state.units].reverse().find(unit => unit.status === "succeeded");
|
|
410
452
|
const current = succeeded === undefined ? undefined : {
|
|
@@ -1114,21 +1156,24 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1114
1156
|
} };
|
|
1115
1157
|
const startMission = `${profile.toolPrefix}start_mission`, planUnits = `${profile.toolPrefix}plan_units`, reviewMission = `${profile.toolPrefix}review_mission`, submitMission = `${profile.toolPrefix}submit_mission`, completeMission = `${profile.toolPrefix}complete_mission`, expandUnit = `${profile.toolPrefix}expand_unit`;
|
|
1116
1158
|
const stringList = { type: "array", items: { type: "string" } };
|
|
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.",
|
|
1159
|
+
tools[startMission] = { description: "Operator: save a few one-line requirements/negative constraints. The original user message, IDs and ownership are captured automatically. A cancelled same-turn run's accepted criteria are restored from host state in their exact order before dispatch. Dispatch the returned Coordinator task immediately, or use plan_units for a simple single-unit Fast-lane task.",
|
|
1118
1160
|
args: { requirements: { ...stringList, minItems: 1, maxItems: 64 } }, execute: async (args, context) => {
|
|
1119
1161
|
await requireRoot(context.sessionID);
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1162
|
+
return serializeDispatchTransition(context.sessionID, async () => {
|
|
1163
|
+
await reconcileMissionDispatch(context.sessionID);
|
|
1164
|
+
const mission = await retainCancelledMissionAcceptance(context.sessionID, await missions.start(context.sessionID, args.requirements));
|
|
1165
|
+
return JSON.stringify(mission.dispatchOpen
|
|
1166
|
+
? missionDispatchPacket(mission, await operators.read(context.sessionID))
|
|
1167
|
+
: { mission_id: mission.id, requirements: mission.requirements, task: missions.task(mission),
|
|
1168
|
+
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." });
|
|
1169
|
+
});
|
|
1126
1170
|
} };
|
|
1127
1171
|
async function declareMissionUnits(root, actor, mission, raw, reason) {
|
|
1172
|
+
const previous = await operators.read(root);
|
|
1173
|
+
mission = await retainCancelledMissionAcceptance(root, mission, previous);
|
|
1128
1174
|
const plan = missionPlan(mission, raw);
|
|
1129
1175
|
if (actor === root && (mission.coordinator !== null || plan.units.length !== 1))
|
|
1130
1176
|
throw new Error("mission-coordinator-required: dispatch the returned Coordinator task");
|
|
1131
|
-
const previous = await operators.read(root);
|
|
1132
1177
|
const same = previous?.planHash === createHash("sha256").update(JSON.stringify(plan)).digest("hex");
|
|
1133
1178
|
if (previous && !["completed", "cancelled"].includes(previous.phase) && !same) {
|
|
1134
1179
|
if (!reason?.trim())
|
|
@@ -1138,8 +1183,19 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1138
1183
|
const budget = await control.currentBudget(root);
|
|
1139
1184
|
if (budget && budget.remaining_units < plan.units.length && !same)
|
|
1140
1185
|
throw new Error("mission-budget-exhausted: report the required cumulative extension to Operator");
|
|
1186
|
+
// Cancellation marks the durable units before interrupting their native sessions. A cancelled
|
|
1187
|
+
// status alone therefore cannot prove the old Worker stopped or its reservation settled.
|
|
1188
|
+
const cancelledPredecessor = previous?.phase === "cancelled" &&
|
|
1189
|
+
(mission.supersededRunID === previous.runID ||
|
|
1190
|
+
(mission.supersededRunID === undefined && previous.decision === "explicit-cancellation" &&
|
|
1191
|
+
previous.units.some(unit => unit.status === "cancelled" && unit.childSessionID !== null)));
|
|
1192
|
+
const terminalChildren = cancelledPredecessor
|
|
1193
|
+
? await terminalCancelledMissionChildren(profile, root, previous, budget, {
|
|
1194
|
+
get: async (id) => payload(await session("get", { path: { id }, query: { directory: input.directory } })),
|
|
1195
|
+
children: async (id) => payload(await session("children", { path: { id }, query: { directory: input.directory } })),
|
|
1196
|
+
}) : [];
|
|
1141
1197
|
const state = await operators.prepareMission(root, plan, actor === root ? undefined
|
|
1142
|
-
: { sessionID: actor, callID: mission.callID }, mission.supersededRunID);
|
|
1198
|
+
: { sessionID: actor, callID: mission.callID }, mission.supersededRunID, terminalChildren);
|
|
1143
1199
|
await restorePriorAcceptance(root, state);
|
|
1144
1200
|
await control.registerGoalDeclaration(root, state.units[0].task.prompt, true);
|
|
1145
1201
|
control.enableUnits(root, state.units.filter(unit => unit.status === "pending").length);
|
|
@@ -1193,7 +1249,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1193
1249
|
const task = risk.length === 0 ? null : { subagent_type: profileAgent(profile, "dog-reviewer"),
|
|
1194
1250
|
description: `🔎 ${run.units[0].unit.title}`, prompt: [
|
|
1195
1251
|
`candidate_id: ${mission.id}`, `review_phase: ${phase}`, "canonical_validation_exit: 0", `risk_tags: [${risk.join(", ")}]`,
|
|
1196
|
-
"Review this candidate independently. Use the language of the requirements/traces. Invoke no tools. First line: exactly PASS or
|
|
1252
|
+
"Review this candidate independently. Use the language of the requirements/traces. Invoke no tools. First line: exactly PASS, FINDINGS or EVIDENCE_GAPS.",
|
|
1253
|
+
"Use EVIDENCE_GAPS only when no finding establishes a source/test defect and every finding is proof the supplied artifact cannot settle, including process history later established on the base. Any concrete defect uses FINDINGS.",
|
|
1197
1254
|
`acceptance: ${JSON.stringify(run.acceptance)}`, `changedLogicSummary: ${JSON.stringify(traces)}`,
|
|
1198
1255
|
...run.acceptance.map((_, i) => `acceptance[${i}] -> changedLogicSummary[${i}]`),
|
|
1199
1256
|
`manifest: ${JSON.stringify(run.units.map(unit => unit.unit))}`, `sourceFingerprint: ${source.fingerprint}`,
|
|
@@ -1202,13 +1259,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1202
1259
|
].join("\n") };
|
|
1203
1260
|
const reviewed = await missions.update(root, item => {
|
|
1204
1261
|
item.review = { runID: run.runID, risk: risk, source: source.fingerprint,
|
|
1205
|
-
task, verdict: task ? "pending" : "skipped-low-risk", ...(mission.review?.child ? { child: mission.review.child } : {})
|
|
1262
|
+
task, verdict: task ? "pending" : "skipped-low-risk", ...(mission.review?.child ? { child: mission.review.child } : {}),
|
|
1263
|
+
...(mission.review?.evidenceGapReviews ? { evidenceGapReviews: mission.review.evidenceGapReviews } : {}) };
|
|
1206
1264
|
});
|
|
1207
1265
|
return JSON.stringify(task ? { status: "review-required", task: missionReviewTask(reviewed) } : { status: "skipped-low-risk" });
|
|
1208
1266
|
} };
|
|
1209
1267
|
async function assertMissionReview(root, mission) {
|
|
1210
1268
|
const run = await operators.required(root), review = mission.review;
|
|
1211
|
-
if (!review || review.runID !== run.runID || !
|
|
1269
|
+
if (!review || review.runID !== run.runID || !missionReviewAccepted(review) ||
|
|
1212
1270
|
review.source !== (await missionReviewSource(input.directory, run)).fingerprint)
|
|
1213
1271
|
throw new Error("mission-review-required-or-stale");
|
|
1214
1272
|
}
|
|
@@ -1736,13 +1794,23 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1736
1794
|
const history = child ? await messages(child) : [];
|
|
1737
1795
|
const last = [...history].reverse().find(message => (record(message.info) ? message.info.role : message.role) === "assistant");
|
|
1738
1796
|
const text = Array.isArray(last?.parts) ? last.parts.filter(record).filter(part => part.type === "text").map(part => part.text).join("\n") : String(raw);
|
|
1739
|
-
|
|
1797
|
+
const independent = who?.role === "dog-reviewer" && who.parent === ownership.actor;
|
|
1798
|
+
const reviewed = await missions.update(ownership.root, mission => {
|
|
1740
1799
|
if (mission.review) {
|
|
1741
1800
|
mission.review.result = text.slice(0, 16_000);
|
|
1742
|
-
|
|
1801
|
+
const verdict = missionReviewVerdict(text);
|
|
1802
|
+
mission.review.verdict = independent ? verdict : "findings";
|
|
1803
|
+
if (mission.review.verdict === "evidence-gaps")
|
|
1804
|
+
mission.review.evidenceGapReviews = (mission.review.evidenceGapReviews ?? 0) + 1;
|
|
1743
1805
|
mission.review.child = child;
|
|
1744
1806
|
}
|
|
1745
1807
|
});
|
|
1808
|
+
if (reviewed.review?.verdict === "evidence-gaps" && typeof output.output === "string") {
|
|
1809
|
+
const count = reviewed.review.evidenceGapReviews ?? 0;
|
|
1810
|
+
output.output += missionReviewAccepted(reviewed.review)
|
|
1811
|
+
? `\n\nHOST: evidence-gap review ${count}/${MISSION_EVIDENCE_GAP_REVIEW_LIMIT} reached the limit. No defect was found; submit_mission ready is permitted. List the remaining evidence gaps in the summary.`
|
|
1812
|
+
: `\n\nHOST: evidence-gap review ${count}/${MISSION_EVIDENCE_GAP_REVIEW_LIMIT}. No defect was found; supply the requested evidence through review_mission traces (or at most one evidence-only unit) and re-review. Do not re-implement.`;
|
|
1813
|
+
}
|
|
1746
1814
|
}
|
|
1747
1815
|
taskOwners.delete(request.callID);
|
|
1748
1816
|
return;
|
|
@@ -1847,8 +1915,20 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1847
1915
|
},
|
|
1848
1916
|
"experimental.chat.system.transform": async (request, output) => {
|
|
1849
1917
|
const root = await rootFor(request.sessionID) ?? await proposalPromptRoot(request.sessionID);
|
|
1850
|
-
if (!root)
|
|
1918
|
+
if (!root) {
|
|
1919
|
+
const who = await identity(request.sessionID);
|
|
1920
|
+
if (who.parent === undefined && who.role === undefined &&
|
|
1921
|
+
(await missions.read(request.sessionID))?.phase === "cancelled") {
|
|
1922
|
+
// OpenCode retains the same conversation when the user switches agents. A prior
|
|
1923
|
+
// INTERRUPTED report describes only its Sortie run; it cannot revoke Build's
|
|
1924
|
+
// native tools or turn a new independent request into a cancelled mission.
|
|
1925
|
+
(output.system ??= []).push("SORTIE_PROFILE_INACTIVE: This is a non-Sortie agent. " +
|
|
1926
|
+
"Any earlier INTERRUPTED mission or cancelled operator run in this session is historical state of the Sortie profile, " +
|
|
1927
|
+
"not a restriction on this agent's current user request. Use the current agent's native tools for independent work. " +
|
|
1928
|
+
"Do not revive, overwrite or claim completion of that historical Sortie run.");
|
|
1929
|
+
}
|
|
1851
1930
|
return;
|
|
1931
|
+
}
|
|
1852
1932
|
await core["experimental.chat.system.transform"]?.(request, output);
|
|
1853
1933
|
(output.system ??= []).push(`SORTIE_RUNTIME_PROFILE ${profile.id}; marker ${assetVersion}. ` +
|
|
1854
1934
|
`Shared MkII protocol role names are logical: ${protocolMap}. Use only ${profile.toolPrefix} tools for this profile. ` +
|
|
@@ -27,6 +27,7 @@ export interface RuntimeBridge {
|
|
|
27
27
|
continuationCheckpoint?(rootSessionID: string): Promise<string | undefined>;
|
|
28
28
|
requiresExplicitAcceptance?(rootSessionID: string): Promise<boolean>;
|
|
29
29
|
ownsCanonicalValidation?(rootSessionID: string, unitID: string, childSessionID: string, command: string): Promise<boolean>;
|
|
30
|
+
onHostHandoffRepaired?(rootSessionID: string, taskID: string, handoffPath: string, original: string, repaired: string): Promise<void>;
|
|
30
31
|
allowsInvestigativeShell?(sessionID: string): Promise<boolean>;
|
|
31
32
|
onSerialSettlement?(settlement: SerialDispatchSettlement): Promise<void>;
|
|
32
33
|
onRootTerminal?(rootSessionID: string, receipt: GoalTerminalReceipt): Promise<void>;
|
package/dist/plugin/v2.js
CHANGED
|
@@ -63,13 +63,14 @@ function legacyMessage(value, session, agent) {
|
|
|
63
63
|
if (!record(value) || typeof value.id !== "string" || typeof value.type !== "string")
|
|
64
64
|
return undefined;
|
|
65
65
|
const id = value.id;
|
|
66
|
+
const originalAgent = record(value.metadata) ? string(value.metadata.agent) ?? agent : agent;
|
|
66
67
|
if (value.type === "user") {
|
|
67
|
-
return { info: { id, sessionID: session, role: "user", agent, time: value.time },
|
|
68
|
+
return { info: { id, sessionID: session, role: "user", agent: originalAgent, time: value.time },
|
|
68
69
|
parts: [{ id: `${id}-text`, sessionID: session, messageID: id, type: "text", text: String(value.text ?? "") }] };
|
|
69
70
|
}
|
|
70
71
|
if (value.type !== "assistant") {
|
|
71
72
|
if (value.type === "synthetic")
|
|
72
|
-
return { info: { id, sessionID: session, role: "user", agent, time: value.time },
|
|
73
|
+
return { info: { id, sessionID: session, role: "user", agent: originalAgent, time: value.time },
|
|
73
74
|
parts: [{ id: `${id}-text`, sessionID: session, messageID: id, type: "text", text: String(value.text ?? ""), synthetic: true }] };
|
|
74
75
|
return undefined;
|
|
75
76
|
}
|
|
@@ -432,13 +432,20 @@ routes separately from mutation routes; an enumeration justified only by the req
|
|
|
432
432
|
covers mutation routes while leaving creation routes unlisted, is incomplete. Require the value representations
|
|
433
433
|
that reach the rule to be listed too, and require each trace to exercise the form with the new rule active; a
|
|
434
434
|
trace whose case leaves the new rule inactive evidences only the pre-existing behavior. A missing enumeration, an
|
|
435
|
-
excluded form without a reason, or an enumerated form without a trace is
|
|
436
|
-
|
|
435
|
+
excluded form without a reason, or an enumerated form without a trace is never PASS. When the supplied
|
|
436
|
+
excerpts cannot settle the form and no defect is established, it is an evidence gap, not a FINDINGS defect.
|
|
437
437
|
|
|
438
438
|
Reject a matrix that lists independent syntax or dispatch dimensions but traces them only in isolation. Require
|
|
439
439
|
the material combinations that can select different branches, including cardinality with optional-clause
|
|
440
440
|
presence, scope, and value representation where applicable. For a multi-target route, require evidence for every
|
|
441
441
|
target; proving only the first target is a concrete asymmetry finding.
|
|
442
|
+
|
|
443
|
+
## Mission review verdict
|
|
444
|
+
|
|
445
|
+
Start with exactly one of PASS, FINDINGS or EVIDENCE_GAPS. Use FINDINGS when any finding establishes a
|
|
446
|
+
source/test defect or an observed behavior contradicting an acceptance item. Use EVIDENCE_GAPS when every finding
|
|
447
|
+
is proof the artifact cannot settle: a missing excerpt, trace, enumeration, or process history later established
|
|
448
|
+
on the base. List all gaps you see in one response rather than one per round; the host bounds evidence rounds.
|
|
442
449
|
`;
|
|
443
450
|
export const runtimeAssets = Object.freeze([
|
|
444
451
|
...canonicalAssets.map((asset) => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type RuntimeProfile } from "./core/runtime-profile.ts";
|
|
2
|
+
/** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
|
|
3
|
+
export declare const TOOL_ENVIRONMENT = ".sortie-env";
|
|
2
4
|
export declare function missionOperatorContent(profile: RuntimeProfile, version: string): string;
|
|
3
5
|
export declare function missionCoordinatorContent(profile: RuntimeProfile, version: string): string;
|
|
4
6
|
export declare function missionWorkerContent(profile: RuntimeProfile): string;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { profileAgent } from "./core/runtime-profile.js";
|
|
2
2
|
import { STRATEGY_TRIGGERS, SOURCE_REVIEW_RISK_TAGS } from "./core/consultation.js";
|
|
3
3
|
import { SCOUT_EVIDENCE_CODES } from "./core/scout-contract.js";
|
|
4
|
+
/** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
|
|
5
|
+
export const TOOL_ENVIRONMENT = ".sortie-env";
|
|
4
6
|
function controls(profile, names) {
|
|
5
7
|
return names.map(name => ` ${profile.toolPrefix}${name}: true`).join("\n");
|
|
6
8
|
}
|
|
@@ -36,8 +38,9 @@ quality threshold and explicit model/budget choice. Follow AGENTS.md and use the
|
|
|
36
38
|
unit boundaries, Worker/Scout/Advisor/independent Reviewer calls, write-scope extensions and corrections
|
|
37
39
|
within the original request and cumulative budget. Do not investigate or approve each unit at the root.
|
|
38
40
|
3. Compare the returned completion candidate against the original request, real source and observed evidence.
|
|
39
|
-
If incomplete, resume the SAME Coordinator with concrete feedback. If complete and required review passed
|
|
40
|
-
|
|
41
|
+
If incomplete, resume the SAME Coordinator with concrete feedback. If complete and required review passed
|
|
42
|
+
(or the host accepted it at the evidence-gap limit, with the gaps reported), call
|
|
43
|
+
${profile.toolPrefix}complete_mission. Only its succeeded receipt authorizes DONE.
|
|
41
44
|
|
|
42
45
|
Fast-lane exception: simple, low-risk, single-unit work with known files and validation may use
|
|
43
46
|
${profile.toolPrefix}plan_units directly after start_mission, then dispatch its exact Worker task.
|
|
@@ -91,7 +94,10 @@ Investigate only enough to start the first useful Worker. Prefer a targeted read
|
|
|
91
94
|
inventory or speculative full design. Call ${profile.toolPrefix}plan_units with concise units:
|
|
92
95
|
title, objective, read/write file or directory scopes, validation commands, optionally requirement_ids.
|
|
93
96
|
Keep all original requirements covered; omitted requirement_ids means all. Last validation command proves
|
|
94
|
-
that unit.
|
|
97
|
+
that unit. If your quick check shows repository-declared dependencies or the test runner are missing, keep
|
|
98
|
+
setup inside the first unit: declare its checks through the repository-local tool environment ${TOOL_ENVIRONMENT}/
|
|
99
|
+
(for Python, ${TOOL_ENVIRONMENT}/bin/python -m pytest ...) and let that Worker create it. Never plan a separate setup
|
|
100
|
+
unit or put ${TOOL_ENVIRONMENT}/ in a write scope. Host generates IDs, proof mapping, handoff, manifest and Task references. Dispatch the returned
|
|
95
101
|
${profileAgent(profile, "dog-worker")} task verbatim, in foreground. V2 maps subagent_type to agent and
|
|
96
102
|
task_id to sessionID. Do not insert model overrides unless the user explicitly selected them.
|
|
97
103
|
|
|
@@ -114,8 +120,10 @@ After formal validation, call review_mission with risk_tags and one concise impl
|
|
|
114
120
|
requirement. Recognized tags: ${SOURCE_REVIEW_RISK_TAGS.join(", ")}.
|
|
115
121
|
High-risk changes require the generated independent ${profileAgent(profile, "dog-reviewer")} task.
|
|
116
122
|
Low risk uses [] and the host records the skip. The host supplies source excerpts, manifest, requirement
|
|
117
|
-
mapping and validation evidence; do not handwrite that envelope. Fix concrete
|
|
118
|
-
Worker and rerun affected validation/review.
|
|
123
|
+
mapping and validation evidence; do not handwrite that envelope. Fix concrete FINDINGS defects yourself
|
|
124
|
+
through Worker and rerun affected validation/review. EVIDENCE_GAPS means missing proof, not a defect: answer
|
|
125
|
+
it with sharper traces in the next review_mission, or at most one evidence-only unit, never a re-implementation.
|
|
126
|
+
The host caps evidence-only reviews; at its limit, submit ready and list the remaining gaps.
|
|
119
127
|
Preserve candidate lineage and independence; your own opinion or Worker PASS is not independent review.
|
|
120
128
|
|
|
121
129
|
Call submit_mission only for: ready (complete candidate with evidence/review), needs-decision (only the user
|
|
@@ -140,7 +148,8 @@ The binding remains valid throughout this Task until return or a control/source
|
|
|
140
148
|
|
|
141
149
|
Read/search and read-only investigation commands are unrestricted. Use targeted reproduction/diagnosis
|
|
142
150
|
without registering every exploratory command. All writes, generated/transient files and cleanup stay
|
|
143
|
-
inside the unit's file/directory scopes. Do not write outside them through
|
|
151
|
+
inside the unit's file/directory scopes, except the tool environment below. Do not write outside them through
|
|
152
|
+
scripts or tools. If scope must
|
|
144
153
|
expand or a formal check must change, return the exact paths/command and reason to Coordinator; it can
|
|
145
154
|
approve an in-request extension immediately. Do not ask the user or delegate to another agent.
|
|
146
155
|
|
|
@@ -150,6 +159,12 @@ and separate shell calls; the host records actual command, source and exit. Diag
|
|
|
150
159
|
formal acceptance evidence. Do not repeat a failed command without a concrete source/setup correction or
|
|
151
160
|
repeat passed checks on unchanged source. Add meaningful tests only when needed by the change/request.
|
|
152
161
|
|
|
162
|
+
Missing repository-declared dependencies or test runner are setup, not a result. Make one bounded,
|
|
163
|
+
repository-documented setup attempt in the repository-local tool environment ${TOOL_ENVIRONMENT}/ (for Python:
|
|
164
|
+
python -m venv ${TOOL_ENVIRONMENT}, then install the declared dependencies with its pip), then run the checks.
|
|
165
|
+
Reuse an existing ${TOOL_ENVIRONMENT}/ and never delete it; it is local tooling, not a change, and needs no write
|
|
166
|
+
scope. Return to Coordinator for setup only when it is externally blocked or a formal command must change.
|
|
167
|
+
|
|
153
168
|
Return changed paths and concise criterion-level implementation/test/input evidence with the real check
|
|
154
169
|
results. Respect negative constraints and API success/error compatibility. A broad suite PASS does not
|
|
155
170
|
alone prove every requirement; leave unproven items explicit. Never claim other units or the whole mission
|