sortie-dogs 0.12.8 → 0.12.10
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/core/goal-bound.d.ts +2 -0
- package/dist/core/goal-bound.js +2 -1
- package/dist/core/initialize.js +8 -2
- package/dist/core/operator-mission.d.ts +3 -2
- package/dist/core/operator-mission.js +23 -10
- package/dist/core/operator-runtime.d.ts +7 -1
- package/dist/core/operator-runtime.js +52 -29
- package/dist/plugin/goal-completion.d.ts +17 -0
- package/dist/plugin/goal-completion.js +55 -0
- package/dist/plugin/index.js +23 -114
- package/dist/plugin/mission-location.d.ts +26 -0
- package/dist/plugin/mission-location.js +38 -0
- package/dist/plugin/mission-review.d.ts +7 -0
- package/dist/plugin/mission-review.js +108 -10
- package/dist/plugin/profiled.d.ts +1 -1
- package/dist/plugin/profiled.js +135 -55
- package/dist/plugin/protected-snapshot.d.ts +18 -0
- package/dist/plugin/protected-snapshot.js +98 -0
- package/dist/plugin/runtime-bridge.d.ts +4 -0
- package/dist/plugin/v2-session-history.d.ts +3 -0
- package/dist/plugin/v2-session-history.js +23 -0
- package/dist/plugin/v2.d.ts +5 -0
- package/dist/plugin/v2.js +65 -56
- package/dist/runtime-assets-v010.js +1 -1
- package/dist/runtime-mission-assets.js +11 -0
- package/package.json +2 -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.10-mission-continuity-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.10-mission-continuity-v1";
|
|
@@ -37,6 +37,8 @@ export interface GoalEvidence {
|
|
|
37
37
|
readonly manifest_path: string;
|
|
38
38
|
readonly source_paths: readonly string[];
|
|
39
39
|
readonly candidate_paths: readonly string[];
|
|
40
|
+
/** Missing on legacy evidence: retain its original all-paths snapshot recipe. */
|
|
41
|
+
readonly source_policy?: "project-files-v1";
|
|
40
42
|
};
|
|
41
43
|
readonly execution: {
|
|
42
44
|
readonly command: readonly string[];
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -61,7 +61,8 @@ export function validGoalEvidence(value, state) {
|
|
|
61
61
|
const protectedBindingValid = protectedBinding !== undefined && HASH.test(protectedBinding.manifest_hash) &&
|
|
62
62
|
text(protectedBinding.project_root) && text(protectedBinding.manifest_path) && Array.isArray(protectedBinding.source_paths) &&
|
|
63
63
|
protectedBinding.source_paths.every(text) && Array.isArray(protectedBinding.candidate_paths) &&
|
|
64
|
-
protectedBinding.candidate_paths.every(text)
|
|
64
|
+
protectedBinding.candidate_paths.every(text) &&
|
|
65
|
+
(protectedBinding.source_policy === undefined || protectedBinding.source_policy === "project-files-v1");
|
|
65
66
|
const matches = criteria.length > 0 && criteria.length === value.measurement.criterion_ids.length &&
|
|
66
67
|
criteria.every((criterion) => criterion.target === value.measurement.target &&
|
|
67
68
|
criterion.entrypoint === value.measurement.entrypoint && criterion.workload === value.measurement.workload &&
|
package/dist/core/initialize.js
CHANGED
|
@@ -379,8 +379,14 @@ async function v010Config(root, global) {
|
|
|
379
379
|
}
|
|
380
380
|
const options = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
|
|
381
381
|
const update = (path, value) => { content = applyEdits(content, modify(content, path, value, options)); };
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
const wrapper = `${prefix}plugins/sortie-dogs/index.js`;
|
|
383
|
+
const wrapperSource = await readFile(resolve(root, wrapper), "utf8").catch(() => "");
|
|
384
|
+
const localV2Wrapper = /^\s*export\s*\{\s*default\s*\}\s*from\s*["']sortie-dogs\/server["'];?\s*$/u.test(wrapperSource);
|
|
385
|
+
const registered = plugins?.some(item => {
|
|
386
|
+
const name = typeof item === "string" ? item : item !== null && typeof item === "object" ? item.package : undefined;
|
|
387
|
+
return typeof name === "string" && /^sortie-dogs(?:@[^/]+)?$/u.test(name);
|
|
388
|
+
});
|
|
389
|
+
if (!localV2Wrapper && !registered) {
|
|
384
390
|
update(["plugins"], [...(plugins ?? []), "sortie-dogs"]);
|
|
385
391
|
}
|
|
386
392
|
if (depth === undefined || depth < 2)
|
|
@@ -40,6 +40,7 @@ export interface OperatorMission {
|
|
|
40
40
|
risk: string[];
|
|
41
41
|
source: string;
|
|
42
42
|
task: OperatorTask | null;
|
|
43
|
+
requestFingerprint?: string;
|
|
43
44
|
verdict: "pending" | "PASS" | "findings" | "evidence-gaps" | "skipped-low-risk";
|
|
44
45
|
result?: string;
|
|
45
46
|
child?: string;
|
|
@@ -75,8 +76,8 @@ export declare class OperatorMissionRuntime {
|
|
|
75
76
|
capture(root: string, request: MissionRequest): Promise<void>;
|
|
76
77
|
start(root: string, requirements: unknown): Promise<OperatorMission>;
|
|
77
78
|
update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
|
|
78
|
-
/** Keep
|
|
79
|
-
carryForward(root: string, missionID: string, acceptance: readonly string[]): Promise<OperatorMission>;
|
|
79
|
+
/** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
|
|
80
|
+
carryForward(root: string, missionID: string, acceptance: readonly string[], supersededRunID?: string): Promise<OperatorMission>;
|
|
80
81
|
task(state: OperatorMission): OperatorTask;
|
|
81
82
|
admit(root: string, callID: string, args: Record<string, unknown>): Promise<OperatorMission>;
|
|
82
83
|
/** Reopen only the exact dispatch whose native Task has a terminal record after a server restart. */
|
|
@@ -201,10 +201,10 @@ export class OperatorMissionRuntime {
|
|
|
201
201
|
return state;
|
|
202
202
|
});
|
|
203
203
|
}
|
|
204
|
-
/** Keep
|
|
205
|
-
carryForward(root, missionID, acceptance) {
|
|
204
|
+
/** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
|
|
205
|
+
carryForward(root, missionID, acceptance, supersededRunID) {
|
|
206
206
|
return this.update(root, state => {
|
|
207
|
-
if (state.id !== missionID || state.supersededRunID !==
|
|
207
|
+
if (state.id !== missionID || state.supersededRunID !== supersededRunID || state.runID !== null ||
|
|
208
208
|
!["open", "running"].includes(state.phase) || (state.dispatchOpen && state.coordinator === null) ||
|
|
209
209
|
acceptance.length === 0 || acceptance.some(text => typeof text !== "string" || !text.trim())) {
|
|
210
210
|
throw new Error("mission-acceptance-carry-forward-unavailable");
|
|
@@ -229,7 +229,11 @@ export class OperatorMissionRuntime {
|
|
|
229
229
|
const resume = state.coordinator !== null && args.task_id === state.coordinator && typeof args.prompt === "string" && args.prompt.trim();
|
|
230
230
|
if (["cancelled", "completed"].includes(state.phase) || state.dispatchOpen || args.subagent_type !== expected.subagent_type ||
|
|
231
231
|
(!resume && (args.prompt !== expected.prompt || args.description !== expected.description || args.task_id))) {
|
|
232
|
-
|
|
232
|
+
const reason = ["cancelled", "completed"].includes(state.phase) ? `mission is ${state.phase}`
|
|
233
|
+
: state.dispatchOpen ? "Coordinator Task is still admitted; read operator_status to reconcile its native completion"
|
|
234
|
+
: args.task_id && args.task_id !== state.coordinator ? `this location owns Coordinator ${state.coordinator ?? "not yet claimed"}, not ${String(args.task_id)}`
|
|
235
|
+
: "use the exact Coordinator task returned by operator_status in this mission's project_root";
|
|
236
|
+
throw new Error(`mission-dispatch-not-authorized: ${reason}`);
|
|
233
237
|
}
|
|
234
238
|
state.phase = "running";
|
|
235
239
|
state.callID = callID;
|
|
@@ -327,21 +331,30 @@ export function missionPlan(mission, raw) {
|
|
|
327
331
|
units });
|
|
328
332
|
}
|
|
329
333
|
export function missionPacket(mission, run) {
|
|
334
|
+
const currentReview = mission.review !== undefined && mission.review.runID === (run?.runID ?? mission.runID);
|
|
335
|
+
const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
|
|
330
336
|
return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
|
|
331
337
|
requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
|
|
332
338
|
submission: mission.submission, progress: mission.progress,
|
|
333
339
|
review: mission.review ? { risk_tags: mission.review.risk, verdict: mission.review.verdict,
|
|
340
|
+
run_id: mission.review.runID, current_run: currentReview,
|
|
334
341
|
source_fingerprint: mission.review.source, reviewer_session_id: mission.review.child ?? null,
|
|
335
342
|
result: mission.review.result ?? null, evidence_gap_reviews: mission.review.evidenceGapReviews ?? 0,
|
|
336
|
-
evidence_gap_review_limit: MISSION_EVIDENCE_GAP_REVIEW_LIMIT, accepted:
|
|
343
|
+
evidence_gap_review_limit: MISSION_EVIDENCE_GAP_REVIEW_LIMIT, accepted: reviewAccepted,
|
|
344
|
+
passed: currentReview && mission.review.verdict === "PASS", permits_submission: reviewAccepted } : null,
|
|
337
345
|
...(run ? { run_id: run.runID, status: run.phase, decision: run.decision,
|
|
338
346
|
units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
|
|
339
347
|
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence })) } : {}),
|
|
340
|
-
next_action:
|
|
341
|
-
:
|
|
342
|
-
? "
|
|
343
|
-
: "Coordinator:
|
|
344
|
-
|
|
348
|
+
next_action: mission.phase === "completed" ? "Mission completed. Report the accepted result and retained review gaps; no further dispatch or completion call is needed."
|
|
349
|
+
: mission.phase === "submitted" && mission.submission?.status === "ready"
|
|
350
|
+
? "Operator: compare the submitted candidate with the original requirements and actual evidence, then complete_mission if satisfied. Report remaining evidence gaps; they are not a review PASS."
|
|
351
|
+
: run?.phase === "awaiting-decision" ? "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative."
|
|
352
|
+
: run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
|
|
353
|
+
? "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."
|
|
354
|
+
: reviewAccepted
|
|
355
|
+
? "Coordinator: review permits submission. Submit the candidate with any remaining gaps; do not repeat passed validation or review. Operator performs final acceptance."
|
|
356
|
+
: "Coordinator: address recorded findings or obtain the required independent review, then submit_mission. Operator compares all requirements with source/evidence before complete_mission.")
|
|
357
|
+
: "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
|
|
345
358
|
}
|
|
346
359
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
347
360
|
export function missionReviewTask(mission) {
|
|
@@ -207,6 +207,8 @@ export interface OperatorState {
|
|
|
207
207
|
decision: string | null;
|
|
208
208
|
receipt: GoalTerminalReceipt | null;
|
|
209
209
|
}
|
|
210
|
+
/** A later mission may replace an unexecuted cancellation, but must retain settled work's acceptance. */
|
|
211
|
+
export declare function cancelledMissionRetainsAcceptance(state: OperatorState): boolean;
|
|
210
212
|
export declare function operatorGitPathAuthorized(path: string, scopes: readonly string[], platform?: NodeJS.Platform): boolean;
|
|
211
213
|
export declare function parseOperatorPlan(value: unknown): OperatorPlan;
|
|
212
214
|
/** Root-owned durable queue. It never executes code, changes acceptance, or accepts a candidate. */
|
|
@@ -246,7 +248,11 @@ export declare class OperatorRuntime {
|
|
|
246
248
|
sessionID: string;
|
|
247
249
|
callID: string;
|
|
248
250
|
}, supersededRunID?: string, terminalChildren?: readonly string[]): Promise<OperatorState>;
|
|
249
|
-
|
|
251
|
+
/** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
|
|
252
|
+
replanMission(root: string, runID: string, raw: unknown, dispatcher?: {
|
|
253
|
+
sessionID: string;
|
|
254
|
+
callID: string;
|
|
255
|
+
}): Promise<OperatorState>;
|
|
250
256
|
private prepareOnce;
|
|
251
257
|
operatorTask(state: OperatorState): OperatorTask;
|
|
252
258
|
/** Root-visible handle for the bounded operations delegate; its contract stays host-internal. */
|
|
@@ -32,6 +32,11 @@ const REPAIR_VALIDATION_STALE_PROOF_DECISION = "operator-contract-repair-validat
|
|
|
32
32
|
const PROCESS_REPLACEMENT_DECISION = "operator-process-remediation-replacement-required";
|
|
33
33
|
const ACCEPTANCE_REMEDIATION_DECISION = "operator-acceptance-remediation-required";
|
|
34
34
|
const REVIEW_REMEDIATION_DECISION = "operator-review-remediation-required";
|
|
35
|
+
/** A later mission may replace an unexecuted cancellation, but must retain settled work's acceptance. */
|
|
36
|
+
export function cancelledMissionRetainsAcceptance(state) {
|
|
37
|
+
return state.phase === "cancelled" && (state.decision === "agent-changed" || state.priorAcceptedUnits.length > 0 ||
|
|
38
|
+
state.units.some(unit => ["succeeded", "failed"].includes(unit.status) || unit.evidence.length > 0));
|
|
39
|
+
}
|
|
35
40
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
36
41
|
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
37
42
|
const identifier = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
|
|
@@ -170,7 +175,7 @@ export function parseOperatorPlan(value) {
|
|
|
170
175
|
for (const [unitIndex, unit] of value.units.entries()) {
|
|
171
176
|
if (!record(unit) || !exactKeys(unit, ["id", "title", "objective", "read", "write", "validation", "acceptance_indices"]) ||
|
|
172
177
|
!identifier(unit.id) || ids.has(unit.id) || !text(unit.title) || !text(unit.objective) ||
|
|
173
|
-
!strings(unit.read) || !strings(unit.write
|
|
178
|
+
!strings(unit.read) || !strings(unit.write) || !strings(unit.validation, true))
|
|
174
179
|
return planError(`/units/${unitIndex}`, "operator-unit-invalid", "operator-unit-shape");
|
|
175
180
|
unit.validation.forEach((command, index) => rejectValidationAnnotation(command, `/units/${unitIndex}/validation/${index}`));
|
|
176
181
|
ids.add(unit.id);
|
|
@@ -681,19 +686,9 @@ export class OperatorRuntime {
|
|
|
681
686
|
prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = []) {
|
|
682
687
|
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren }));
|
|
683
688
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (state.units.some(unit => unit.status === "running") || state.gitLifecycle !== null)
|
|
688
|
-
throw new Error("mission-replan-worker-still-active");
|
|
689
|
-
if (["cancelled", "completed"].includes(state.phase))
|
|
690
|
-
return;
|
|
691
|
-
// Keep the old run and proof for inspection. Replanning does not erase failed checks or spend.
|
|
692
|
-
await writeFile(`${this.file(root)}.${state.runID}.archive`, JSON.stringify(state), { flag: "wx", mode: 0o600 });
|
|
693
|
-
state.phase = "cancelled";
|
|
694
|
-
state.decision = "mission-replan";
|
|
695
|
-
await this.save(state);
|
|
696
|
-
});
|
|
689
|
+
/** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
|
|
690
|
+
replanMission(root, runID, raw, dispatcher) {
|
|
691
|
+
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, replaceRunID: runID }));
|
|
697
692
|
}
|
|
698
693
|
async prepareOnce(root, raw, scopeApprovalTurnID, mission) {
|
|
699
694
|
const previous = await this.read(root);
|
|
@@ -720,6 +715,16 @@ export class OperatorRuntime {
|
|
|
720
715
|
}
|
|
721
716
|
const plan = parseOperatorPlan(immutableReplacement);
|
|
722
717
|
const validatedPlanHash = hash(JSON.stringify(plan));
|
|
718
|
+
const replanning = mission?.replaceRunID !== undefined;
|
|
719
|
+
if (replanning) {
|
|
720
|
+
if (!previous || previous.runID !== mission.replaceRunID || ["cancelled", "completed"].includes(previous.phase)) {
|
|
721
|
+
throw new Error("mission-replan-run-mismatch");
|
|
722
|
+
}
|
|
723
|
+
if (previous.units.some(unit => unit.status === "running") || previous.gitLifecycle !== null)
|
|
724
|
+
throw new Error("mission-replan-worker-still-active");
|
|
725
|
+
if (previous.planHash === validatedPlanHash)
|
|
726
|
+
return previous;
|
|
727
|
+
}
|
|
723
728
|
if (mission?.supersededRunID !== undefined && previous?.supersededRunID === mission.supersededRunID &&
|
|
724
729
|
previous.planHash === validatedPlanHash && previous.phase !== "cancelled")
|
|
725
730
|
return previous;
|
|
@@ -728,29 +733,36 @@ export class OperatorRuntime {
|
|
|
728
733
|
if (mission?.supersededRunID !== undefined && !superseding)
|
|
729
734
|
throw new Error("mission-superseded-run-mismatch");
|
|
730
735
|
const terminalChildren = mission?.terminalChildren ?? [];
|
|
731
|
-
const
|
|
736
|
+
const retainAcceptance = previous !== undefined && cancelledMissionRetainsAcceptance(previous);
|
|
737
|
+
const predecessorChildren = previous?.units.flatMap(unit => unit.childSessionID !== null
|
|
732
738
|
? [unit.childSessionID] : []) ?? [];
|
|
733
|
-
if (superseding && previous && (previous.decision
|
|
734
|
-
previous.repairResidualPaths.length > 0 || previous.
|
|
735
|
-
terminalChildren.length !==
|
|
736
|
-
|
|
737
|
-
|
|
739
|
+
if (superseding && previous && (!["explicit-cancellation", "agent-changed"].includes(previous.decision ?? "") || previous.gitLifecycle !== null ||
|
|
740
|
+
previous.repairResidualPaths.length > 0 || previous.contractRepair !== null ||
|
|
741
|
+
terminalChildren.length !== predecessorChildren.length || new Set(terminalChildren).size !== terminalChildren.length ||
|
|
742
|
+
predecessorChildren.some(id => !terminalChildren.includes(id)) || previous.units.some(unit => !(retainAcceptance ? ["pending", "cancelled", "succeeded", "failed"] : ["pending", "cancelled"]).includes(unit.status) ||
|
|
743
|
+
(!retainAcceptance && (unit.evidence.length > 0 || unit.resultClass !== null)) ||
|
|
744
|
+
unit.repairValidation !== null || (unit.status === "pending" && (unit.callID !== null || unit.childSessionID !== null ||
|
|
745
|
+
unit.evidence.length > 0 || unit.resultClass !== null)) ||
|
|
746
|
+
(unit.status === "succeeded" && (unit.childSessionID === null || unit.callID === null || unit.evidence.length === 0 || unit.resultClass !== "acceptance")) ||
|
|
738
747
|
(unit.status === "cancelled" && (unit.childSessionID === null || unit.callID === null))))) {
|
|
739
748
|
throw new Error("mission-superseded-run-has-work: reconcile prior workers and evidence before changing acceptance");
|
|
740
749
|
}
|
|
741
750
|
if (plan.git_lifecycle !== undefined) {
|
|
742
751
|
await ensureGitManagedStateExcluded(this.projectRoot, this.profile, this.gitPath);
|
|
743
752
|
}
|
|
744
|
-
if (previous && !["completed", "cancelled"].includes(previous.phase)) {
|
|
753
|
+
if (previous && !replanning && !["completed", "cancelled"].includes(previous.phase)) {
|
|
745
754
|
if (previous.planHash === validatedPlanHash)
|
|
746
755
|
return previous;
|
|
747
756
|
throw new Error("operator-active-contract-immutable");
|
|
748
757
|
}
|
|
749
758
|
// Cancellation stops execution, not the accepted user order. A replacement
|
|
750
759
|
// plan must carry the original ordered criteria; only a completed root clears them.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
760
|
+
// Treat the predecessor as retired only in this staged replacement. Its durable state remains
|
|
761
|
+
// intact through schema checks, control writes and archive creation, until save atomically switches it.
|
|
762
|
+
const parent = replanning ? { ...previous, phase: "cancelled", decision: "mission-replan" }
|
|
763
|
+
: (!superseding || retainAcceptance) && previous?.phase === "cancelled" ? previous : undefined;
|
|
764
|
+
if (parent && ["explicit-cancellation", "agent-changed"].includes(parent.decision ?? "")) {
|
|
765
|
+
const cancelled = parent.units.flatMap(unit => (parent.decision === "agent-changed" || unit.status === "cancelled") && unit.childSessionID !== null
|
|
754
766
|
? [unit.childSessionID] : []);
|
|
755
767
|
if (cancelled.length > 0 && (new Set(terminalChildren).size !== terminalChildren.length ||
|
|
756
768
|
cancelled.some(id => !terminalChildren.includes(id)))) {
|
|
@@ -888,13 +900,15 @@ export class OperatorRuntime {
|
|
|
888
900
|
const contents = [JSON.stringify(handoff), JSON.stringify(manifest)];
|
|
889
901
|
controls.push({ path: handoffPath, content: contents[0] }, { path: manifestPath, content: contents[1] });
|
|
890
902
|
const prompt = ["role: implementation", `task_id: ${taskID}`, `project_root: ${this.projectRoot}`,
|
|
891
|
-
`source_manifest: ${unit.write.join(", ")}`, `operation_manifest: ${manifestRelative}`, `handoff_path: ${handoffPath}`,
|
|
903
|
+
`source_manifest: ${(unit.write.length ? unit.write : unit.read).join(", ") || "none"}`, `operation_manifest: ${manifestRelative}`, `handoff_path: ${handoffPath}`,
|
|
892
904
|
`goal_declaration_path: ${declarationPath}`, "acceptance:", ...plan.acceptance.map(value => ` - ${value}`),
|
|
893
905
|
"validation:", ...unit.validation.map(value => ` - ${value}`),
|
|
894
906
|
mission ? "Read-only investigation commands are unrestricted. Use shell to reproduce and diagnose without asking for command registration. Keep all writes, including generated/transient outputs and cleanup, inside unit.write. Run formal validation exactly as listed, in order and in separate calls, so the host records its real result. If a write scope or formal check must change, return the precise change to your Coordinator; it can extend/redeclare immediately within the original requirements. Diagnostic success is not formal acceptance evidence."
|
|
895
907
|
: "Execute validation in its declared order. Earlier entries may be approved generator, build, formatter, or exact cleanup commands required before canonical criterion tests. Every persistent or transient generator output must be declared in unit.write. Cleanup may remove only declared unit.write outputs and must be an explicit ordered command after generation and before post-commit or canonical validation; never add an ignore rule or remove an undeclared path. If any necessary command, input, output, or cleanup is missing, do not run an undeclared command or variant and do not use resume evidence tooling to invent permission; return a contract-repair decision.",
|
|
896
908
|
"Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation.",
|
|
897
|
-
|
|
909
|
+
mission
|
|
910
|
+
? "Do not spawn nested subagents for consultation. Required technical or user decisions that must precede implementation belong to the root before dispatch; use the confirmed decisions and evidence declared in the unit objective and inputs. If a required prior decision is missing, return the exact contract gap to the parent instead of implementing dependent changes, inventing consent, or asking the user to repeat an already recorded decision. Independent SourceReview is a separate post-implementation and post-validation review: do not require it before implementation or formal validation, and do not stop this Task solely because it has not yet occurred. Complete the assigned implementation and all listed formal validation commands, then return the real evidence so the Coordinator can dispatch the independent review_mission with applicable risk_tags (for example, public-logic). Do not conduct or delegate that independent review yourself. Reflect supplied prior Reviewer FINDINGS within this unit's scope."
|
|
911
|
+
: "Do not spawn nested subagents for consultation. Required consultations belong to the root before dispatch; use the confirmed decisions and evidence declared in the unit objective and inputs. If required consultation results or user decisions are missing, return the exact contract gap to the parent instead of attempting a deeper Task, inventing consent, or asking the user to repeat an already recorded decision.",
|
|
898
912
|
...(plan.git_lifecycle !== undefined && index === plan.units.length - 1 ? [
|
|
899
913
|
`git_post_commit_validation: ${JSON.stringify(plan.git_lifecycle.post_commit_validation)}`,
|
|
900
914
|
"Complete every source write before invoking any git_post_commit_validation command. Its first exact invocation is the host commit boundary; after it, source mutation and undeclared shell commands are denied. Run every listed command and return its real evidence.",
|
|
@@ -935,10 +949,19 @@ export class OperatorRuntime {
|
|
|
935
949
|
await handle.close();
|
|
936
950
|
}
|
|
937
951
|
}
|
|
938
|
-
if (superseding && previous) {
|
|
939
|
-
|
|
952
|
+
if ((superseding || replanning) && previous) {
|
|
953
|
+
// A crash before the state switch may leave a checkpoint while the old run later advances.
|
|
954
|
+
// Sequence-qualified replan snapshots keep that history without blocking a subsequent correction.
|
|
955
|
+
const archive = `${this.file(root)}.${previous.runID}${replanning ? `.${previous.sequence}` : ""}.archive`;
|
|
940
956
|
try {
|
|
941
|
-
await
|
|
957
|
+
const handle = await open(archive, "wx", 0o600);
|
|
958
|
+
created.push(archive);
|
|
959
|
+
try {
|
|
960
|
+
await handle.writeFile(JSON.stringify(previous));
|
|
961
|
+
}
|
|
962
|
+
finally {
|
|
963
|
+
await handle.close();
|
|
964
|
+
}
|
|
942
965
|
}
|
|
943
966
|
catch (error) {
|
|
944
967
|
if (error.code !== "EEXIST" ||
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type GoalEvidence, type GoalFlightState } from "../core/goal-bound.js";
|
|
2
|
+
export interface CompletionBlocker {
|
|
3
|
+
readonly reason: "active-reservations" | "missing-contract" | "missing-evidence" | "missing-binding" | "snapshot-unavailable" | "source-changed" | "candidate-changed";
|
|
4
|
+
readonly criterion_id?: string;
|
|
5
|
+
readonly source_paths?: readonly string[];
|
|
6
|
+
readonly candidate_paths?: readonly string[];
|
|
7
|
+
readonly expected?: string;
|
|
8
|
+
readonly actual?: string;
|
|
9
|
+
readonly legacy_control_snapshot?: boolean;
|
|
10
|
+
readonly next_action: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CompletionReadiness {
|
|
13
|
+
readonly ready: boolean;
|
|
14
|
+
readonly blockers: readonly CompletionBlocker[];
|
|
15
|
+
}
|
|
16
|
+
/** The same read-only decision drives completion and public status, without settling or rerunning work. */
|
|
17
|
+
export declare function goalCompletionReadiness(state: GoalFlightState, evidence: readonly GoalEvidence[]): Promise<CompletionReadiness>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { validGoalEvidence } from "../core/goal-bound.js";
|
|
2
|
+
import { isRuntimeControlPath, refreshProtectedSnapshot } from "./protected-snapshot.js";
|
|
3
|
+
/** The same read-only decision drives completion and public status, without settling or rerunning work. */
|
|
4
|
+
export async function goalCompletionReadiness(state, evidence) {
|
|
5
|
+
const blockers = [];
|
|
6
|
+
if (state.outstanding_reservations.length)
|
|
7
|
+
blockers.push({ reason: "active-reservations",
|
|
8
|
+
next_action: "Reconcile the outstanding native tasks before acceptance; do not repeat completion while work is reserved." });
|
|
9
|
+
if (!state.goal_id || !state.acceptance_fingerprint || !state.acceptance_contract?.criteria.length) {
|
|
10
|
+
blockers.push({ reason: "missing-contract", next_action: "Restore the current run's registered goal and unchanged requirements before completion." });
|
|
11
|
+
}
|
|
12
|
+
const snapshots = new Map();
|
|
13
|
+
for (const criterion of state.acceptance_contract?.criteria ?? []) {
|
|
14
|
+
const proof = [...evidence].reverse().find(entry => entry.measurement.criterion_ids.includes(criterion.criterion_id) && validGoalEvidence(entry, state));
|
|
15
|
+
if (!state.satisfied_criteria.includes(criterion.criterion_id) || !proof) {
|
|
16
|
+
blockers.push({ reason: "missing-evidence", criterion_id: criterion.criterion_id,
|
|
17
|
+
next_action: "Coordinator: obtain the missing criterion evidence within the existing requirements and cumulative budget; retain already valid checks." });
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (criterion.source_binding !== "current-protected" && criterion.candidate_binding !== "current-protected")
|
|
21
|
+
continue;
|
|
22
|
+
const binding = proof.protected_binding;
|
|
23
|
+
if (!binding) {
|
|
24
|
+
blockers.push({ reason: "missing-binding", criterion_id: criterion.criterion_id,
|
|
25
|
+
next_action: "Coordinator: obtain host-observed validation bound to this criterion; prose cannot replace a missing snapshot." });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const key = JSON.stringify(binding);
|
|
29
|
+
let pending = snapshots.get(key);
|
|
30
|
+
if (!pending) {
|
|
31
|
+
pending = refreshProtectedSnapshot(binding.project_root, binding).catch(() => undefined);
|
|
32
|
+
snapshots.set(key, pending);
|
|
33
|
+
}
|
|
34
|
+
const current = await pending;
|
|
35
|
+
const scope = { criterion_id: criterion.criterion_id, source_paths: binding.source_paths, candidate_paths: binding.candidate_paths };
|
|
36
|
+
if (!current) {
|
|
37
|
+
blockers.push({ ...scope, reason: "snapshot-unavailable",
|
|
38
|
+
next_action: "Inspect the pinned manifest and declared paths for missing, changed or unreadable files. Correct the identified cause before validation or completion; do not retry unchanged." });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (criterion.source_binding === "current-protected" && current.source !== proof.identity.source) {
|
|
42
|
+
const legacy = !binding.source_policy && binding.source_paths.some(path => path === "" || path === "." || isRuntimeControlPath(path));
|
|
43
|
+
blockers.push({ ...scope, reason: "source-changed", expected: proof.identity.source, actual: current.source,
|
|
44
|
+
...(legacy ? { legacy_control_snapshot: true } : {}),
|
|
45
|
+
next_action: legacy
|
|
46
|
+
? "Legacy evidence includes mutable runtime/Git records. Coordinator: obtain fresh evidence with the current snapshot policy in the same mission and cumulative budget. Do not edit the ledger, reset requirements, or repeat completion unchanged."
|
|
47
|
+
: "Coordinator: inspect the changed source scopes and validate only the affected criterion after the candidate is settled; do not repeat unrelated passed checks or completion unchanged." });
|
|
48
|
+
}
|
|
49
|
+
if (criterion.candidate_binding === "current-protected" && current.candidate !== proof.identity.candidate) {
|
|
50
|
+
blockers.push({ ...scope, reason: "candidate-changed", expected: proof.identity.candidate, actual: current.candidate,
|
|
51
|
+
next_action: "Coordinator: the declared output changed after validation. Validate the intended current candidate and update its review before final acceptance; do not repeat completion unchanged." });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { ready: blockers.length === 0, blockers };
|
|
55
|
+
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import { lstat, mkdir, open, readFile,
|
|
10
|
+
import { lstat, mkdir, open, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
11
11
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
12
12
|
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";
|
|
@@ -18,7 +18,7 @@ import { admitLunaFabric } from "../core/luna-fabric-contract.js";
|
|
|
18
18
|
import { summarizeExperienceEvidence } from "../core/experience-evidence-summary.js";
|
|
19
19
|
import { selectExperienceRoute } from "../core/experience-route-policy.js";
|
|
20
20
|
import { resolveExperienceRouting } from "../core/experience-routing-runtime.js";
|
|
21
|
-
import {
|
|
21
|
+
import { normalizeRelativePath, RelativePathError } from "../core/path.js";
|
|
22
22
|
import { ScopeLeaseError, ScopeLeaseRegistry } from "../core/scope-lease-registry.js";
|
|
23
23
|
import { produceWorktreeCommitArtifact, recoverWorktreeCommitArtifact, resolveValidationExecutable, WorktreeCommitArtifactError, } from "../core/worktree-commit-artifact.js";
|
|
24
24
|
import { normalizeWorktreeScope } from "../core/worktree-scope.js";
|
|
@@ -51,6 +51,8 @@ import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetri
|
|
|
51
51
|
import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
52
52
|
import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
|
|
53
53
|
import { receiptBoundTerminalText } from "./receipt-presentation.js";
|
|
54
|
+
import { protectedSnapshot, refreshProtectedSnapshot } from "./protected-snapshot.js";
|
|
55
|
+
import { goalCompletionReadiness } from "./goal-completion.js";
|
|
54
56
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
55
57
|
const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
56
58
|
const ACTIVE_SESSION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
@@ -233,94 +235,6 @@ async function durableScopeRoot(projectRoot) {
|
|
|
233
235
|
return undefined;
|
|
234
236
|
}
|
|
235
237
|
}
|
|
236
|
-
async function protectedScopeDigest(projectRoot, paths, manifestHash) {
|
|
237
|
-
const entries = [];
|
|
238
|
-
const canonicalRoot = await realpath(projectRoot);
|
|
239
|
-
const visit = async (absolute) => {
|
|
240
|
-
const scoped = relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
241
|
-
if (scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped))
|
|
242
|
-
return false;
|
|
243
|
-
const metadata = await lstat(absolute).catch((error) => {
|
|
244
|
-
if (isRecord(error) && error.code === "ENOENT")
|
|
245
|
-
return undefined;
|
|
246
|
-
throw error;
|
|
247
|
-
});
|
|
248
|
-
if (metadata === undefined) {
|
|
249
|
-
entries.push([scoped, "missing"]);
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
if (metadata.isSymbolicLink()) {
|
|
253
|
-
// Package managers place executable links under node_modules/.bin. Record
|
|
254
|
-
// both the link and its target bytes, but never follow a link out of the
|
|
255
|
-
// project or recurse through a linked directory (which may form a cycle).
|
|
256
|
-
const target = await realpath(absolute).catch(() => undefined);
|
|
257
|
-
if (target === undefined)
|
|
258
|
-
return false;
|
|
259
|
-
const relativeTarget = relative(canonicalRoot, target);
|
|
260
|
-
if (relativeTarget === ".." || relativeTarget.startsWith("../") || isAbsolute(relativeTarget) ||
|
|
261
|
-
!(await stat(target)).isFile())
|
|
262
|
-
return false;
|
|
263
|
-
const link = await readlink(absolute);
|
|
264
|
-
const contents = await readFile(target);
|
|
265
|
-
entries.push([scoped, `symlink:${link}`, createHash("sha256").update(contents).digest("hex")]);
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
if (metadata.isDirectory()) {
|
|
269
|
-
entries.push([scoped, "directory"]);
|
|
270
|
-
const children = await readdir(absolute);
|
|
271
|
-
for (const child of children.sort())
|
|
272
|
-
if (!await visit(join(absolute, child)))
|
|
273
|
-
return false;
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
if (!metadata.isFile())
|
|
277
|
-
return false;
|
|
278
|
-
entries.push([scoped, "file", createHash("sha256").update(await readFile(absolute)).digest("hex")]);
|
|
279
|
-
return true;
|
|
280
|
-
};
|
|
281
|
-
for (const path of [...new Set(paths)].sort())
|
|
282
|
-
if (!await visit(path))
|
|
283
|
-
return undefined;
|
|
284
|
-
return goalFingerprint({ manifest_hash: `sha256:${manifestHash}`, entries });
|
|
285
|
-
}
|
|
286
|
-
async function protectedSnapshot(authorization) {
|
|
287
|
-
const manifestSource = await readFile(authorization.manifestPath).catch(() => undefined);
|
|
288
|
-
if (manifestSource === undefined)
|
|
289
|
-
return undefined;
|
|
290
|
-
const manifestHash = createHash("sha256").update(manifestSource).digest("hex");
|
|
291
|
-
if (manifestHash !== authorization.manifestHash)
|
|
292
|
-
return undefined;
|
|
293
|
-
const relativePath = relative(authorization.projectRoot, authorization.manifestPath).replaceAll("\\", "/");
|
|
294
|
-
const manifest = JSON.parse(manifestSource.toString("utf8"));
|
|
295
|
-
const actualPaths = (entries) => entries.map((entry) => {
|
|
296
|
-
const path = normalizeManifestPath(entry);
|
|
297
|
-
return path.kind === "relative" ? resolve(authorization.projectRoot, path.path) : resolve(path.path);
|
|
298
|
-
});
|
|
299
|
-
// Ownership keys may be case-folded even on POSIX (for example DrvFS). Hash the actual manifest paths.
|
|
300
|
-
const candidatePaths = actualPaths(manifest.write);
|
|
301
|
-
const sourcePaths = [...new Set([...actualPaths(manifest.read), ...candidatePaths])];
|
|
302
|
-
const source = await protectedScopeDigest(authorization.projectRoot, sourcePaths, manifestHash);
|
|
303
|
-
const candidate = await protectedScopeDigest(authorization.projectRoot, candidatePaths, manifestHash);
|
|
304
|
-
if (source === undefined || candidate === undefined || relativePath.startsWith("../") || isAbsolute(relativePath))
|
|
305
|
-
return undefined;
|
|
306
|
-
return { binding: { manifest_hash: `sha256:${manifestHash}`, project_root: authorization.projectRoot,
|
|
307
|
-
manifest_path: relativePath,
|
|
308
|
-
source_paths: sourcePaths.map((path) => relative(authorization.projectRoot, path).replaceAll("\\", "/")),
|
|
309
|
-
candidate_paths: candidatePaths.map((path) => relative(authorization.projectRoot, path).replaceAll("\\", "/")) },
|
|
310
|
-
source, candidate };
|
|
311
|
-
}
|
|
312
|
-
async function refreshProtectedSnapshot(projectRoot, binding) {
|
|
313
|
-
const manifestPath = resolve(projectRoot, binding.manifest_path);
|
|
314
|
-
const manifestSource = await readFile(manifestPath).catch(() => undefined);
|
|
315
|
-
if (manifestSource === undefined)
|
|
316
|
-
return undefined;
|
|
317
|
-
const manifestHash = createHash("sha256").update(manifestSource).digest("hex");
|
|
318
|
-
if (`sha256:${manifestHash}` !== binding.manifest_hash)
|
|
319
|
-
return undefined;
|
|
320
|
-
const source = await protectedScopeDigest(projectRoot, binding.source_paths.map((path) => resolve(projectRoot, path)), manifestHash);
|
|
321
|
-
const candidate = await protectedScopeDigest(projectRoot, binding.candidate_paths.map((path) => resolve(projectRoot, path)), manifestHash);
|
|
322
|
-
return source === undefined || candidate === undefined ? undefined : { source, candidate };
|
|
323
|
-
}
|
|
324
238
|
function isRecord(value) {
|
|
325
239
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
326
240
|
}
|
|
@@ -1919,34 +1833,23 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1919
1833
|
origin_user_message_id: ticket.origin_user_message_id,
|
|
1920
1834
|
} } };
|
|
1921
1835
|
}
|
|
1922
|
-
async function terminalGoal(sessionID, stopReason, status, explicitAcceptance = false) {
|
|
1836
|
+
async function terminalGoal(sessionID, stopReason, status, explicitAcceptance = false, onReadiness) {
|
|
1923
1837
|
const ledger = await goalLedger(sessionID);
|
|
1924
|
-
const state =
|
|
1925
|
-
if (state.
|
|
1926
|
-
return state.receipt
|
|
1838
|
+
const { state, records } = await ledger.readGoal();
|
|
1839
|
+
if (state.receipt !== null)
|
|
1840
|
+
return state.receipt;
|
|
1927
1841
|
if (status === "succeeded" && !explicitAcceptance &&
|
|
1928
1842
|
await input.runtimeBridge?.requiresExplicitAcceptance?.(sessionID) === true)
|
|
1929
1843
|
return undefined;
|
|
1930
|
-
const records = (await ledger.readGoal()).records;
|
|
1931
1844
|
const settledEvidence = records.flatMap(({ event }) => event.kind === "unit.settled" || event.kind === "unit.evidence-reconciled" ? event.evidence : []);
|
|
1932
|
-
if (status === "succeeded"
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
for (const criterion of state.acceptance_contract.criteria) {
|
|
1938
|
-
if (criterion.source_binding !== "current-protected" && criterion.candidate_binding !== "current-protected")
|
|
1939
|
-
continue;
|
|
1940
|
-
const evidence = [...settledEvidence].reverse().find((entry) => entry.measurement.criterion_ids.includes(criterion.criterion_id) && validGoalEvidence(entry, state));
|
|
1941
|
-
if (evidence?.protected_binding === undefined)
|
|
1942
|
-
return undefined;
|
|
1943
|
-
const current = await refreshProtectedSnapshot(evidence.protected_binding.project_root, evidence.protected_binding).catch(() => undefined);
|
|
1944
|
-
if (current === undefined ||
|
|
1945
|
-
(criterion.source_binding === "current-protected" && current.source !== evidence.identity.source) ||
|
|
1946
|
-
(criterion.candidate_binding === "current-protected" && current.candidate !== evidence.identity.candidate))
|
|
1947
|
-
return undefined;
|
|
1948
|
-
}
|
|
1845
|
+
if (status === "succeeded") {
|
|
1846
|
+
const readiness = await goalCompletionReadiness(state, settledEvidence);
|
|
1847
|
+
onReadiness?.(readiness);
|
|
1848
|
+
if (!readiness.ready)
|
|
1849
|
+
return undefined;
|
|
1949
1850
|
}
|
|
1851
|
+
if (state.goal_id === null || state.outstanding_reservations.length > 0 || state.acceptance_fingerprint === null)
|
|
1852
|
+
return undefined;
|
|
1950
1853
|
const started = [...records].reverse().find(({ event }) => event.kind === "goal.accepted")?.event.at ?? new Date().toISOString();
|
|
1951
1854
|
const ended = new Date().toISOString();
|
|
1952
1855
|
const startTime = Date.parse(started);
|
|
@@ -7052,6 +6955,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7052
6955
|
prompts.push(part.state.input.prompt);
|
|
7053
6956
|
}
|
|
7054
6957
|
}
|
|
6958
|
+
prompts.push(...await input.runtimeBridge?.completedReviewPrompts?.(toolInput.sessionID, output.args.prompt) ?? []);
|
|
7055
6959
|
fastLane.restoreReviewLineage(toolInput.sessionID, output.args.prompt, prompts);
|
|
7056
6960
|
}
|
|
7057
6961
|
const resumedWorkerSessionID = fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
|
|
@@ -8005,6 +7909,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8005
7909
|
return !snapshot.records.some(({ event }) => event.kind === "dispatch.reserved" && event.reservation_id === expected);
|
|
8006
7910
|
},
|
|
8007
7911
|
recoverUnitEvidence,
|
|
7912
|
+
completionReadiness: async (root) => {
|
|
7913
|
+
const { state, records } = await (await goalLedger(root)).readGoal();
|
|
7914
|
+
return goalCompletionReadiness(state, records.flatMap(({ event }) => event.kind === "unit.settled" || event.kind === "unit.evidence-reconciled" ? event.evidence : []));
|
|
7915
|
+
},
|
|
8008
7916
|
completeRoot: async (sessionID, acceptanceFingerprint) => {
|
|
8009
7917
|
if (!isCoordinatorSession(sessionID) && !await recoverCoordinatorRoot(sessionID))
|
|
8010
7918
|
throw new Error("operator-coordinator-required");
|
|
@@ -8016,9 +7924,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8016
7924
|
throw new Error("operator-goal-stopped");
|
|
8017
7925
|
// Same proof/freshness/reservation gate used by terminal text, requested
|
|
8018
7926
|
// explicitly by the root instead of inferred from a model's wording.
|
|
8019
|
-
|
|
7927
|
+
let completion;
|
|
7928
|
+
const receipt = goal.receipt ?? await terminalGoal(sessionID, "completed", "succeeded", true, result => { completion = result; });
|
|
8020
7929
|
if (receipt?.status !== "succeeded")
|
|
8021
|
-
return { status: "awaiting-evidence" };
|
|
7930
|
+
return { status: "awaiting-evidence", completion };
|
|
8022
7931
|
// Acceptance must return its receipt to the active tool call. Revoking
|
|
8023
7932
|
// continuation timers is not a user-requested session cancellation.
|
|
8024
7933
|
await continuation.stopAutomaticRecovery(sessionID, false, true);
|