sortie-dogs 0.16.1 → 0.16.3
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 +16 -1
- package/dist/core/goal-bound.js +15 -3
- package/dist/core/observed-goal-evidence.d.ts +1 -1
- package/dist/core/observed-goal-evidence.js +2 -2
- package/dist/core/operator-mission.js +4 -4
- package/dist/core/operator-runtime.d.ts +18 -1
- package/dist/core/operator-runtime.js +77 -28
- package/dist/plugin/index.js +22 -5
- package/dist/plugin/mission-review.d.ts +3 -3
- package/dist/plugin/mission-review.js +9 -4
- package/dist/plugin/profiled.js +192 -72
- package/dist/plugin/runtime-bridge.d.ts +11 -1
- package/dist/runtime-mission-assets.js +18 -3
- package/package.json +1 -1
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
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.16.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.16.3-strategy-admission-recovery-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
|
|
8
8
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
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.16.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.16.3-strategy-admission-recovery-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
|
|
@@ -273,6 +273,14 @@ export type GoalFlightEvent = (GoalEventBase & {
|
|
|
273
273
|
readonly goal_id: string;
|
|
274
274
|
readonly revision: number;
|
|
275
275
|
readonly reason: "no-progress";
|
|
276
|
+
}) | (GoalEventBase & {
|
|
277
|
+
readonly kind: "goal.validation-registered";
|
|
278
|
+
readonly goal_id: string;
|
|
279
|
+
readonly reservation_id: string;
|
|
280
|
+
readonly unit_id: string;
|
|
281
|
+
readonly criterion_id: string;
|
|
282
|
+
readonly command: string;
|
|
283
|
+
readonly registered_at: string;
|
|
276
284
|
}) | (GoalEventBase & {
|
|
277
285
|
readonly kind: "goal.terminal";
|
|
278
286
|
readonly goal_id: string;
|
|
@@ -309,6 +317,12 @@ export interface GoalFlightState {
|
|
|
309
317
|
readonly delivery: GoalDeliveryMode | null;
|
|
310
318
|
readonly budget: GoalBudget | null;
|
|
311
319
|
readonly acceptance_contract: GoalAcceptanceContract | null;
|
|
320
|
+
/** Native registration metadata only; the acceptance contract and its fingerprint stay frozen. */
|
|
321
|
+
readonly validation_registrations?: Readonly<Record<string, {
|
|
322
|
+
readonly unit_id: string;
|
|
323
|
+
readonly command: string;
|
|
324
|
+
readonly registered_at: string;
|
|
325
|
+
}>>;
|
|
312
326
|
readonly consumed_units: number;
|
|
313
327
|
readonly consumed_time_ms: number | null;
|
|
314
328
|
readonly consumed_cost_usd: number | null;
|
|
@@ -355,6 +369,7 @@ export declare function selectGoalDelivery(input: {
|
|
|
355
369
|
readonly irreversible_or_major_scope: boolean;
|
|
356
370
|
readonly explicit_mode?: GoalDeliveryMode;
|
|
357
371
|
}): GoalDeliveryMode;
|
|
358
|
-
export declare
|
|
372
|
+
export declare const goalValidationCommand: (state: Pick<GoalFlightState, "validation_registrations">, criterion: GoalAcceptanceCriterion) => string | undefined;
|
|
373
|
+
export declare function validGoalEvidence(value: GoalEvidence, state: Pick<GoalFlightState, "goal_id" | "revision" | "scope_epoch" | "acceptance_fingerprint" | "acceptance_contract" | "validation_registrations">): boolean;
|
|
359
374
|
export declare function reduceGoalFlight(records: readonly GoalFlightEventRecord[]): GoalFlightState;
|
|
360
375
|
export {};
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -51,6 +51,7 @@ function validAcceptanceContract(value) {
|
|
|
51
51
|
function sameStrings(left, right) {
|
|
52
52
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
53
53
|
}
|
|
54
|
+
export const goalValidationCommand = (state, criterion) => state.validation_registrations?.[criterion.criterion_id]?.command ?? criterion.validation_command;
|
|
54
55
|
export function validGoalEvidence(value, state) {
|
|
55
56
|
if (typeof value !== "object" || value === null || typeof value.measurement !== "object" || value.measurement === null ||
|
|
56
57
|
typeof value.identity !== "object" || value.identity === null || typeof value.execution !== "object" || value.execution === null ||
|
|
@@ -100,8 +101,11 @@ export function validGoalEvidence(value, state) {
|
|
|
100
101
|
(criterion.candidate_binding === "current-protected"
|
|
101
102
|
? protectedBindingValid && HASH.test(value.identity.candidate) && (dockerWorkspace === undefined || dockerWorkspace.candidate === value.identity.candidate)
|
|
102
103
|
: criterion.candidate === value.identity.candidate) &&
|
|
103
|
-
(criterion
|
|
104
|
-
(value.execution.command.length === 1 && value.execution.command[0] === criterion
|
|
104
|
+
(goalValidationCommand(state, criterion) === undefined ||
|
|
105
|
+
(value.execution.command.length === 1 && value.execution.command[0] === goalValidationCommand(state, criterion))) &&
|
|
106
|
+
(!state.validation_registrations?.[criterion.criterion_id] ||
|
|
107
|
+
value.execution.units.includes(state.validation_registrations[criterion.criterion_id].unit_id) &&
|
|
108
|
+
Date.parse(value.execution.started_at) >= Date.parse(state.validation_registrations[criterion.criterion_id].registered_at)) &&
|
|
105
109
|
criterion.fixture === value.identity.fixture &&
|
|
106
110
|
criterion.proof_scope === value.proof_scope && criterion.expected_outcome === value.execution.outcome);
|
|
107
111
|
return text(value.evidence_id) && value.goal_id === state.goal_id && value.goal_revision === state.revision &&
|
|
@@ -193,7 +197,7 @@ export function reduceGoalFlight(records) {
|
|
|
193
197
|
selected_agent: event.selected_agent, delivery: event.delivery, budget: event.budget,
|
|
194
198
|
validation_budget: { ...state.validation_budget,
|
|
195
199
|
limit: state.validation_budget.limit === null ? null : Math.max(state.validation_budget.limit, state.validation_budget.consumed, event.budget.max_units) },
|
|
196
|
-
acceptance_contract: event.acceptance_contract,
|
|
200
|
+
acceptance_contract: event.acceptance_contract, validation_registrations: undefined,
|
|
197
201
|
session_ids: addUnique(state.session_ids, event.session_id), phase: "active", stop_reason: null,
|
|
198
202
|
receipt: null, tickets: [], ...(event.reset_no_progress === true
|
|
199
203
|
? { no_progress_results: 0, replan_required: false, replan_used: false }
|
|
@@ -204,6 +208,14 @@ export function reduceGoalFlight(records) {
|
|
|
204
208
|
if (validationAdmissionLimit !== null)
|
|
205
209
|
validationAdmissionLimit = Math.max(event.budget.max_units, state.validation_budget.consumed);
|
|
206
210
|
}
|
|
211
|
+
else if (event.kind === "goal.validation-registered") {
|
|
212
|
+
requireState(state.phase === "active" && state.outstanding_reservations.some(item => item.reservation_id === event.reservation_id && item.unit_id === event.unit_id) &&
|
|
213
|
+
state.acceptance_contract?.criteria.some(item => item.criterion_id === event.criterion_id) === true &&
|
|
214
|
+
commandText(event.command) && instant(event.registered_at) && Date.parse(event.registered_at) <= Date.parse(event.at), "transition", "Validation registration requires the same active reserved unit and a current criterion.");
|
|
215
|
+
state = { ...state, validation_registrations: { ...state.validation_registrations,
|
|
216
|
+
[event.criterion_id]: { unit_id: event.unit_id, command: event.command, registered_at: event.registered_at } },
|
|
217
|
+
satisfied_criteria: state.satisfied_criteria.filter(id => id !== event.criterion_id) };
|
|
218
|
+
}
|
|
207
219
|
else if (event.kind === "ticket.issued") {
|
|
208
220
|
requireState(state.phase === "active" && event.revision === state.revision && event.scope_epoch === state.scope_epoch &&
|
|
209
221
|
event.origin_user_message_id === state.latest_user_message_id && event.sequence === state.tickets.length + 1 &&
|
|
@@ -15,4 +15,4 @@ export interface ObservedGoalExecution {
|
|
|
15
15
|
}
|
|
16
16
|
/** One host execution can exercise several independently declared criteria.
|
|
17
17
|
* Preserve each measurement identity instead of dropping all heterogeneous ones. */
|
|
18
|
-
export declare function evidenceFromObservedExecution(execution: ObservedGoalExecution, state: Pick<GoalFlightState, "goal_id" | "revision" | "scope_epoch" | "acceptance_fingerprint" | "acceptance_contract">, unitID: string): GoalEvidence[];
|
|
18
|
+
export declare function evidenceFromObservedExecution(execution: ObservedGoalExecution, state: Pick<GoalFlightState, "goal_id" | "revision" | "scope_epoch" | "acceptance_fingerprint" | "acceptance_contract" | "validation_registrations">, unitID: string): GoalEvidence[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { goalFingerprint } from "./goal-bound.js";
|
|
1
|
+
import { goalFingerprint, goalValidationCommand } from "./goal-bound.js";
|
|
2
2
|
/** One host execution can exercise several independently declared criteria.
|
|
3
3
|
* Preserve each measurement identity instead of dropping all heterogeneous ones. */
|
|
4
4
|
export function evidenceFromObservedExecution(execution, state, unitID) {
|
|
@@ -6,7 +6,7 @@ export function evidenceFromObservedExecution(execution, state, unitID) {
|
|
|
6
6
|
return [];
|
|
7
7
|
// Canonical requested-full proof is coordinator-owned and must never be manufactured from a
|
|
8
8
|
// child shell result. The caller derives owner from the observed host session lineage.
|
|
9
|
-
const matching = state.acceptance_contract?.criteria.filter(criterion => criterion
|
|
9
|
+
const matching = state.acceptance_contract?.criteria.filter(criterion => goalValidationCommand(state, criterion) === execution.command[0] && criterion.expected_outcome === "pass" &&
|
|
10
10
|
(criterion.proof_scope !== "requested-full" || execution.owner === "coordinator")) ?? [];
|
|
11
11
|
const groups = new Map();
|
|
12
12
|
for (const criterion of matching) {
|
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, open, readFile, readdir, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve, win32 } from "node:path";
|
|
4
4
|
import { normalizeExecutionScope } from "./path.js";
|
|
5
|
-
import { parseOperatorPlan } from "./operator-runtime.js";
|
|
5
|
+
import { parseOperatorPlan, unitValidation } from "./operator-runtime.js";
|
|
6
6
|
import { profileAgent } from "./runtime-profile.js";
|
|
7
7
|
import { normalizeCommand, shellCommandBase, shellCommandWords } from "../plugin/gate.js";
|
|
8
8
|
import { missionValidationCommand } from "./mission-validation-command.js";
|
|
@@ -859,7 +859,7 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
|
|
|
859
859
|
// Settled proof already records these commands. Do not fetch the same native
|
|
860
860
|
// history again merely to render acceptance; freshness is checked separately.
|
|
861
861
|
const proofs = unit.evidence ?? [];
|
|
862
|
-
if (unit.status === "succeeded" && unit.
|
|
862
|
+
if (unit.status === "succeeded" && unitValidation(unit).every(command => proofs.some(proof => proof.execution.exit_code === 0 && proof.execution.outcome === "pass" &&
|
|
863
863
|
proof.execution.command.includes(command)))) {
|
|
864
864
|
return { ...provenance, status: "recorded-formal-evidence",
|
|
865
865
|
evidence_ids: proofs.map(proof => proof.evidence_id),
|
|
@@ -868,7 +868,7 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
|
|
|
868
868
|
try {
|
|
869
869
|
if (!observe || !unit.childSessionID)
|
|
870
870
|
throw new Error("native-worker-history-unavailable");
|
|
871
|
-
return { ...provenance, status: "available", observations: await observe(unit
|
|
871
|
+
return { ...provenance, status: "available", observations: await observe(unitValidation(unit), unit.childSessionID, unit.directExecution ? Date.parse(unit.directExecution.startedAt) :
|
|
872
872
|
unit.reviewerCorrection ? Date.parse(unit.reviewerCorrection.admittedAt ?? item.state.createdAt) : undefined) };
|
|
873
873
|
}
|
|
874
874
|
catch (error) {
|
|
@@ -942,7 +942,7 @@ export function missionPacket(mission, run, includeWorkQuantity = true) {
|
|
|
942
942
|
units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
|
|
943
943
|
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence,
|
|
944
944
|
handoff_path: unit.handoffPath, operation_manifest_path: unit.manifestPath,
|
|
945
|
-
scope_read: unit.unit.read, scope_write: unit.unit.write, validation: unit
|
|
945
|
+
scope_read: unit.unit.read, scope_write: unit.unit.write, validation: unitValidation(unit),
|
|
946
946
|
normal_remediation_used: unit.normalRemediationUsed ?? false,
|
|
947
947
|
...(unit.failure ? { failure: { outcome: unit.failure.outcome, exit_code: unit.failure.exitCode } } : {}),
|
|
948
948
|
...(unit.dispatchDenial ? { dispatch_denial: unit.dispatchDenial } : {}) })) } : {}),
|
|
@@ -127,6 +127,13 @@ interface UnitState {
|
|
|
127
127
|
validationRegisteredAt?: Readonly<Record<string, string>>;
|
|
128
128
|
};
|
|
129
129
|
evidence: readonly GoalEvidence[];
|
|
130
|
+
/** Effective check registration, separate from the immutable admitted contract. */
|
|
131
|
+
validationRegistration?: {
|
|
132
|
+
validation: readonly string[];
|
|
133
|
+
cwd: Readonly<Record<string, string>>;
|
|
134
|
+
registeredAt: Readonly<Record<string, string>>;
|
|
135
|
+
checks: import("../plugin/runtime-bridge.js").ReviewerCorrectionCheck[];
|
|
136
|
+
};
|
|
130
137
|
/** Implementation in the existing controller session, without a native child Task. */
|
|
131
138
|
directExecution?: {
|
|
132
139
|
actor: string;
|
|
@@ -138,6 +145,7 @@ interface UnitState {
|
|
|
138
145
|
previousResultClass: string | null;
|
|
139
146
|
};
|
|
140
147
|
checks: import("../plugin/runtime-bridge.js").ReviewerCorrectionCheck[];
|
|
148
|
+
validation?: readonly string[];
|
|
141
149
|
validationCwd?: Readonly<Record<string, string>>;
|
|
142
150
|
validationRegisteredAt?: Readonly<Record<string, string>>;
|
|
143
151
|
};
|
|
@@ -283,6 +291,10 @@ export interface OperatorProgress {
|
|
|
283
291
|
export declare function cancelledMissionRetainsAcceptance(state: OperatorState): boolean;
|
|
284
292
|
export declare function operatorGitPathAuthorized(path: string, scopes: readonly string[], platform?: NodeJS.Platform): boolean;
|
|
285
293
|
export declare function parseOperatorPlan(value: unknown, scopeFormat?: "repository" | "execution"): OperatorPlan;
|
|
294
|
+
export declare const unitValidation: (unit: OperatorState["units"][number]) => readonly string[];
|
|
295
|
+
export declare const unitValidationCwd: (unit: OperatorState["units"][number]) => Readonly<Record<string, string>> | undefined;
|
|
296
|
+
/** Strip only the owning unit's registration fields when comparing its frozen plan. */
|
|
297
|
+
export declare function directValidationRegistrationPlanHash(state: OperatorState, plan: OperatorPlan, unitID: string): string;
|
|
286
298
|
/** Root-owned durable queue. It never executes code, changes acceptance, or accepts a candidate. */
|
|
287
299
|
export declare class OperatorRuntime {
|
|
288
300
|
readonly profile: RuntimeProfile;
|
|
@@ -332,7 +344,10 @@ export declare class OperatorRuntime {
|
|
|
332
344
|
prepareMission(root: string, raw: unknown, dispatcher?: {
|
|
333
345
|
sessionID: string;
|
|
334
346
|
callID: string;
|
|
335
|
-
}, supersededRunID?: string, terminalChildren?: readonly string[], replaceRequirements?: boolean, context?: Record<string, unknown
|
|
347
|
+
}, supersededRunID?: string, terminalChildren?: readonly string[], replaceRequirements?: boolean, context?: Record<string, unknown>, terminalTaskCalls?: {
|
|
348
|
+
runID: string;
|
|
349
|
+
callIDs: readonly string[];
|
|
350
|
+
}): Promise<OperatorState>;
|
|
336
351
|
/** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
|
|
337
352
|
replanMission(root: string, runID: string, raw: unknown, dispatcher?: {
|
|
338
353
|
sessionID: string;
|
|
@@ -368,6 +383,8 @@ export declare class OperatorRuntime {
|
|
|
368
383
|
}>;
|
|
369
384
|
private matchesWorkerTask;
|
|
370
385
|
matchesRecordedWorkerTask(state: OperatorState, unitID: string, args: unknown): boolean;
|
|
386
|
+
/** Historical comparison only: cancellation revoked the admission generation. */
|
|
387
|
+
matchesCancelledWorkerTask(state: OperatorState, unitID: string, args: unknown): boolean;
|
|
371
388
|
admitOperator(root: string, callID: string, args: unknown): Promise<OperatorTask>;
|
|
372
389
|
private admitOperatorOnce;
|
|
373
390
|
bindOperator(root: string, sessionID: string, prompt: string): Promise<void>;
|
|
@@ -260,6 +260,23 @@ export function parseOperatorPlan(value, scopeFormat = "repository") {
|
|
|
260
260
|
expandGoalDeclaration(value.goal_declaration);
|
|
261
261
|
return { ...value, acceptance: normalizeAcceptanceCriteria(value.acceptance) };
|
|
262
262
|
}
|
|
263
|
+
export const unitValidation = (unit) => unit.validationRegistration?.validation ?? unit.unit.validation;
|
|
264
|
+
export const unitValidationCwd = (unit) => unit.validationRegistration?.cwd ?? unit.directExecution?.validationCwd ?? unit.unit.validation_cwd;
|
|
265
|
+
/** Strip only the owning unit's registration fields when comparing its frozen plan. */
|
|
266
|
+
export function directValidationRegistrationPlanHash(state, plan, unitID) {
|
|
267
|
+
const original = state.units.find(item => item.unit.id === unitID)?.unit;
|
|
268
|
+
const proposed = plan.units.find(item => item.id === unitID);
|
|
269
|
+
const criteria = plan.goal_declaration.criteria;
|
|
270
|
+
const goal = Array.isArray(criteria) ? { ...plan.goal_declaration, criteria: criteria.map(criterion => record(criterion) && criterion.criterion_id === unitID && criterion.validation_command === proposed?.validation.at(-1)
|
|
271
|
+
? { ...criterion, validation_command: original?.validation.at(-1) } : criterion) } : plan.goal_declaration;
|
|
272
|
+
return hash(JSON.stringify({ ...plan, goal_declaration: goal, units: plan.units.map((next, index) => {
|
|
273
|
+
const prior = state.units[index]?.unit;
|
|
274
|
+
if (!prior || prior.id !== unitID || next.id !== unitID)
|
|
275
|
+
return next;
|
|
276
|
+
const { validation_cwd: _cwd, ...rest } = next;
|
|
277
|
+
return { ...rest, validation: prior.validation, ...(prior.validation_cwd ? { validation_cwd: prior.validation_cwd } : {}) };
|
|
278
|
+
}) }));
|
|
279
|
+
}
|
|
263
280
|
/** Root-owned durable queue. It never executes code, changes acceptance, or accepts a candidate. */
|
|
264
281
|
export class OperatorRuntime {
|
|
265
282
|
profile;
|
|
@@ -749,8 +766,8 @@ export class OperatorRuntime {
|
|
|
749
766
|
}
|
|
750
767
|
}
|
|
751
768
|
/** Mission authority is supplied only by the owning profile, never by a model-authored plan. */
|
|
752
|
-
prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = [], replaceRequirements = false, context) {
|
|
753
|
-
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren, replaceRequirements, context }));
|
|
769
|
+
prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = [], replaceRequirements = false, context, terminalTaskCalls) {
|
|
770
|
+
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren, replaceRequirements, context, terminalTaskCalls }));
|
|
754
771
|
}
|
|
755
772
|
/** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
|
|
756
773
|
replanMission(root, runID, raw, dispatcher, context) {
|
|
@@ -766,26 +783,22 @@ export class OperatorRuntime {
|
|
|
766
783
|
unit.directExecution.actor !== actor || unit.directExecution.finishedAt || unit.reviewerCorrection ||
|
|
767
784
|
plan.units.length !== state.units.length)
|
|
768
785
|
throw new Error("mission-validation-registration-owner-mismatch");
|
|
769
|
-
|
|
770
|
-
const prior = state.units[i].unit;
|
|
771
|
-
if (prior.validation_cwd)
|
|
772
|
-
return { ...next, validation_cwd: prior.validation_cwd };
|
|
773
|
-
const { validation_cwd: _cwd, ...rest } = next;
|
|
774
|
-
return rest;
|
|
775
|
-
}) };
|
|
776
|
-
if (hash(JSON.stringify(original)) !== state.planHash || plan.units.some((next, i) => next.id !== unit.unit.id && JSON.stringify(next.validation_cwd) !== JSON.stringify(state.units[i].unit.validation_cwd))) {
|
|
786
|
+
if (directValidationRegistrationPlanHash(state, plan, unit.unit.id) !== state.planHash) {
|
|
777
787
|
throw new Error("mission-validation-registration-contract-mismatch");
|
|
778
788
|
}
|
|
779
|
-
const
|
|
780
|
-
const
|
|
781
|
-
|
|
789
|
+
const next = plan.units.find(next => next.id === unit.unit.id);
|
|
790
|
+
const validation = next.validation.map(normalizeCommand), cwd = next.validation_cwd ?? {};
|
|
791
|
+
const previousValidation = unitValidation(unit), previousCwd = unitValidationCwd(unit) ?? {};
|
|
792
|
+
if (JSON.stringify(validation) !== JSON.stringify(previousValidation) || JSON.stringify(cwd) !== JSON.stringify(previousCwd)) {
|
|
782
793
|
// Keep old checks as history, never attach a new recipe to their old successes.
|
|
783
794
|
const registeredAt = { ...unit.directExecution.validationRegisteredAt }, now = new Date().toISOString();
|
|
784
|
-
for (const command of
|
|
785
|
-
if (
|
|
795
|
+
for (const command of validation) {
|
|
796
|
+
if (previousValidation.filter(item => item === command).length !== validation.filter(item => item === command).length ||
|
|
797
|
+
resolve(this.projectRoot, cwd[command] ?? ".") !== resolve(this.projectRoot, previousCwd[command] ?? ".")) {
|
|
786
798
|
registeredAt[command] = now;
|
|
787
799
|
}
|
|
788
800
|
}
|
|
801
|
+
unit.validationRegistration = { validation, cwd, registeredAt, checks: unit.validationRegistration?.checks ?? [] };
|
|
789
802
|
unit.directExecution.validationCwd = cwd;
|
|
790
803
|
unit.directExecution.validationRegisteredAt = registeredAt;
|
|
791
804
|
await this.save(state);
|
|
@@ -837,9 +850,9 @@ export class OperatorRuntime {
|
|
|
837
850
|
return this.serial(root, async () => {
|
|
838
851
|
const state = await this.required(root);
|
|
839
852
|
const unit = state.units.find(item => /^task_id: (.+)$/mu.exec(item.task.prompt)?.[1] === taskID);
|
|
840
|
-
const execution = unit?.reviewerCorrection ?? unit?.directExecution;
|
|
853
|
+
const execution = unit?.reviewerCorrection ?? unit?.directExecution ?? unit?.validationRegistration;
|
|
841
854
|
if (!unit || !execution || unit.callID !== check.dispatchCallID || unit.childSessionID !== check.childSessionID ||
|
|
842
|
-
(unit.reviewerCorrection?.author ?? unit.directExecution?.actor) !== check.childSessionID)
|
|
855
|
+
(unit.reviewerCorrection?.author ?? unit.directExecution?.actor ?? unit.childSessionID) !== check.childSessionID)
|
|
843
856
|
throw new Error("mission-review-correction-check-owner-mismatch");
|
|
844
857
|
const checks = execution.checks ??= [];
|
|
845
858
|
if (checks.some(item => item.callID === check.callID))
|
|
@@ -906,8 +919,8 @@ export class OperatorRuntime {
|
|
|
906
919
|
plan.units.length !== 1 || plan.acceptance.length !== previous.acceptance.length ||
|
|
907
920
|
previous.acceptance.some((text, i) => text !== plan.acceptance[i]) ||
|
|
908
921
|
JSON.stringify(plan.acceptance_proof) !== JSON.stringify(previous.acceptanceProof) ||
|
|
909
|
-
JSON.stringify(plan.units[0].validation) !== JSON.stringify(previous.units.flatMap(
|
|
910
|
-
JSON.stringify(plan.units[0].validation_cwd ?? {}) !== JSON.stringify(Object.assign({}, ...previous.units.map(item => item
|
|
922
|
+
JSON.stringify(plan.units[0].validation) !== JSON.stringify(previous.units.flatMap(unitValidation)) ||
|
|
923
|
+
JSON.stringify(plan.units[0].validation_cwd ?? {}) !== JSON.stringify(Object.assign({}, ...previous.units.map(item => unitValidationCwd(item) ?? {}))) ||
|
|
911
924
|
JSON.stringify(plan.source_refs) !== JSON.stringify(previous.sourceRefs) ||
|
|
912
925
|
plan.units.some(unit => unit.write.some(path => !this.pathAuthorized(path, mission.reviewerCorrection.writeUnion))))) {
|
|
913
926
|
throw new Error("mission-review-correction-contract-mismatch");
|
|
@@ -933,6 +946,17 @@ export class OperatorRuntime {
|
|
|
933
946
|
if (mission?.supersededRunID !== undefined && !superseding)
|
|
934
947
|
throw new Error("mission-superseded-run-mismatch");
|
|
935
948
|
const terminalChildren = mission?.terminalChildren ?? [];
|
|
949
|
+
const unclaimedCalls = previous?.units.flatMap(unit => !unit.directExecution && unit.status === "cancelled" &&
|
|
950
|
+
unit.childSessionID === null && unit.callID !== null ? [unit.callID] : []) ?? [];
|
|
951
|
+
// A cancelled admission can precede its child's first prompt. Only the owning
|
|
952
|
+
// profile's exact native Task proof, not null or cancellation alone, closes it.
|
|
953
|
+
if (mission && previous?.phase === "cancelled" && unclaimedCalls.length &&
|
|
954
|
+
(mission?.terminalTaskCalls?.runID !== previous.runID ||
|
|
955
|
+
new Set(mission.terminalTaskCalls.callIDs).size !== unclaimedCalls.length ||
|
|
956
|
+
mission.terminalTaskCalls.callIDs.length !== unclaimedCalls.length ||
|
|
957
|
+
unclaimedCalls.some(call => !mission.terminalTaskCalls.callIDs.includes(call)))) {
|
|
958
|
+
throw new Error("mission-cancelled-run-task-not-terminal");
|
|
959
|
+
}
|
|
936
960
|
const retainAcceptance = previous !== undefined && cancelledMissionRetainsAcceptance(previous);
|
|
937
961
|
const predecessorChildren = previous?.units.flatMap(unit => !unit.directExecution && unit.childSessionID !== null
|
|
938
962
|
? [unit.childSessionID] : []) ?? [];
|
|
@@ -944,7 +968,7 @@ export class OperatorRuntime {
|
|
|
944
968
|
unit.repairValidation !== null || (unit.status === "pending" && (unit.callID !== null || unit.childSessionID !== null ||
|
|
945
969
|
unit.evidence.length > 0 || unit.resultClass !== null)) ||
|
|
946
970
|
(unit.status === "succeeded" && (unit.childSessionID === null || unit.callID === null || unit.evidence.length === 0 || unit.resultClass !== "acceptance")) ||
|
|
947
|
-
(unit.status === "cancelled" &&
|
|
971
|
+
(unit.status === "cancelled" && unit.callID === null)))) {
|
|
948
972
|
throw new Error("mission-superseded-run-has-work: reconcile prior workers and evidence before changing acceptance");
|
|
949
973
|
}
|
|
950
974
|
if (plan.git_lifecycle !== undefined) {
|
|
@@ -1184,9 +1208,11 @@ export class OperatorRuntime {
|
|
|
1184
1208
|
await handle.close();
|
|
1185
1209
|
}
|
|
1186
1210
|
}
|
|
1187
|
-
if ((superseding || replanning || previous?.phase === "completed"
|
|
1211
|
+
if ((superseding || replanning || previous?.phase === "completed" ||
|
|
1212
|
+
previous?.phase === "cancelled" && unclaimedCalls.length > 0 && mission?.terminalTaskCalls !== undefined) && previous) {
|
|
1188
1213
|
// A crash before the state switch may leave a checkpoint while the old run later advances.
|
|
1189
1214
|
// Sequence-qualified replan snapshots keep that history without blocking a subsequent correction.
|
|
1215
|
+
// Same-request recovery also retains the original unclaimed cancellation, not just its costs.
|
|
1190
1216
|
const archive = replanning || previous.phase === "completed" ? this.replanArchivePath(previous)
|
|
1191
1217
|
: `${this.file(root)}.${previous.runID}.archive`;
|
|
1192
1218
|
try {
|
|
@@ -1357,6 +1383,28 @@ export class OperatorRuntime {
|
|
|
1357
1383
|
: args;
|
|
1358
1384
|
return unit !== undefined && this.matchesWorkerTask(state, unit, normalized);
|
|
1359
1385
|
}
|
|
1386
|
+
/** Historical comparison only: cancellation revoked the admission generation. */
|
|
1387
|
+
matchesCancelledWorkerTask(state, unitID, args) {
|
|
1388
|
+
const unit = state.units.find(item => item.unit.id === unitID);
|
|
1389
|
+
if (state.phase !== "cancelled" || !unit || unit.status !== "cancelled" || unit.childSessionID !== null ||
|
|
1390
|
+
!record(args) || typeof args.prompt !== "string")
|
|
1391
|
+
return false;
|
|
1392
|
+
if (args.prompt === unit.task.prompt)
|
|
1393
|
+
return this.matchesRecordedWorkerTask(state, unitID, args);
|
|
1394
|
+
if (!args.prompt.startsWith(`${OPERATOR_TASK_REFERENCE} `))
|
|
1395
|
+
return false;
|
|
1396
|
+
let reference;
|
|
1397
|
+
try {
|
|
1398
|
+
reference = JSON.parse(args.prompt.slice(OPERATOR_TASK_REFERENCE.length + 1));
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
if (!record(reference) || !Number.isSafeInteger(reference.g) || reference.g < 1 ||
|
|
1404
|
+
reference.g >= state.generation)
|
|
1405
|
+
return false;
|
|
1406
|
+
return this.matchesRecordedWorkerTask({ ...state, generation: reference.g }, unitID, args);
|
|
1407
|
+
}
|
|
1360
1408
|
admitOperator(root, callID, args) {
|
|
1361
1409
|
return this.serial(root, () => this.admitOperatorOnce(root, callID, args));
|
|
1362
1410
|
}
|
|
@@ -1467,14 +1515,15 @@ export class OperatorRuntime {
|
|
|
1467
1515
|
throw new Error("mission-direct-handoff-unavailable");
|
|
1468
1516
|
}
|
|
1469
1517
|
await this.verifyControls(unit);
|
|
1470
|
-
const segment = { ...structuredClone(unit.directExecution),
|
|
1518
|
+
const segment = { ...structuredClone(unit.directExecution), validation: unitValidation(unit),
|
|
1519
|
+
callID: unit.callID, handedOffAt: new Date().toISOString() };
|
|
1471
1520
|
// Keep the original controls immutable. Publish continuation context and the new
|
|
1472
1521
|
// Task reference together in the atomic run save, so a crash has no half-updated pins.
|
|
1473
1522
|
const continuation = { actor, unit_id: unit.unit.id, at: segment.handedOffAt,
|
|
1474
|
-
instruction: "Continue existing changes and remaining action. The authoritative handoff retains the exact original requirements. Reuse current source-bound checks; inspect missing facts only. Handoff itself is not validation.",
|
|
1523
|
+
instruction: "Continue existing changes and remaining action. The authoritative handoff retains the exact original requirements. This continuation's validation/validation_cwd is the effective registration, superseding only the frozen check recipe. Reuse current source-bound checks; inspect missing facts only. Handoff itself is not validation.",
|
|
1475
1524
|
continuity: context.continuity,
|
|
1476
1525
|
checks: segment.checks.map(({ callID, command, directory, exitCode, startedAt, endedAt }) => ({ callID, command, directory, exitCode, startedAt, endedAt })),
|
|
1477
|
-
|
|
1526
|
+
validation: segment.validation, validation_cwd: unitValidationCwd(unit), details_ref: this.statePath(root) };
|
|
1478
1527
|
unit.task = { ...unit.task, prompt: `${unit.task.prompt.replace(/\ndirect_handoff: .+$/mu, "")}\ndirect_handoff: ${JSON.stringify(continuation)}` };
|
|
1479
1528
|
(unit.directHandoffs ??= []).push(segment);
|
|
1480
1529
|
delete unit.directExecution;
|
|
@@ -1610,7 +1659,7 @@ export class OperatorRuntime {
|
|
|
1610
1659
|
}
|
|
1611
1660
|
async captureContractRepair(state, unit, error, command) {
|
|
1612
1661
|
const normalized = normalizeCommand(command);
|
|
1613
|
-
const commandIndex = unit.
|
|
1662
|
+
const commandIndex = unitValidation(unit).map(normalizeCommand).indexOf(normalized);
|
|
1614
1663
|
if (commandIndex < 0)
|
|
1615
1664
|
throw new Error("operator-contract-repair-validation-command-missing");
|
|
1616
1665
|
const paths = [...new Set(error.diagnostics.flatMap(item => item.repair_paths ?? []))].sort();
|
|
@@ -1630,7 +1679,7 @@ export class OperatorRuntime {
|
|
|
1630
1679
|
return { code: "operator-git-change-outside-write-union", unit_id: unit.unit.id,
|
|
1631
1680
|
diagnostics: structuredClone(error.diagnostics), diagnostics_truncated: error.diagnostics_truncated,
|
|
1632
1681
|
repair_generation: 0, mode, repair_fingerprint: repairFingerprint, files,
|
|
1633
|
-
remaining_validation: unit.
|
|
1682
|
+
remaining_validation: unitValidation(unit).slice(commandIndex).map(normalizeCommand) };
|
|
1634
1683
|
}
|
|
1635
1684
|
recordContractRepair(root, actor, error, command) {
|
|
1636
1685
|
return this.serial(root, async () => {
|
|
@@ -2368,7 +2417,7 @@ export class OperatorRuntime {
|
|
|
2368
2417
|
id: unit.unit.id, status: unit.status, child_session_id: unit.childSessionID, result_class: unit.resultClass,
|
|
2369
2418
|
task_ref: unit.status === "pending" ? (unit.repairValidation === null ? this.workerTask(state, unit).prompt : this.repairWorkerTask(state, unit).prompt) : null,
|
|
2370
2419
|
handoff_path: unit.handoffPath, operation_manifest_path: unit.manifestPath,
|
|
2371
|
-
scope_read: unit.unit.read, scope_write: unit.unit.write, validation: unit
|
|
2420
|
+
scope_read: unit.unit.read, scope_write: unit.unit.write, validation: unitValidation(unit),
|
|
2372
2421
|
acceptance_indices: unit.unit.acceptance_indices,
|
|
2373
2422
|
normal_remediation_used: unit.normalRemediationUsed ?? false,
|
|
2374
2423
|
...(unit.terminalRescue ? { terminal_rescue: { attempt_id: unit.terminalRescue.attempt_id,
|
|
@@ -2398,7 +2447,7 @@ export class OperatorRuntime {
|
|
|
2398
2447
|
unit_id: unit.unit.id, task_id: /^task_id: (.+)$/m.exec(unit.task.prompt)?.[1],
|
|
2399
2448
|
project_root: this.projectRoot, handoff_path: this.controlReference(unit.handoffPath), operation_manifest: this.controlReference(unit.manifestPath),
|
|
2400
2449
|
objective: unit.unit.objective, read: unit.unit.read, write: unit.unit.write,
|
|
2401
|
-
validation: unit
|
|
2450
|
+
validation: unitValidation(unit), acceptance: state.acceptance,
|
|
2402
2451
|
...(unit.repairValidation === null ? {} : { validation_only: true }),
|
|
2403
2452
|
})}\nThis is the durable assignment for this same Worker, including after compaction or reload. ` +
|
|
2404
2453
|
"A missing path in a summary does not mean the handoff is missing. Read the retained handoff/manifest using these project-relative paths without prepending project_root, and inspect the actual diff and outputs before claiming no edits or replaying work. " +
|
package/dist/plugin/index.js
CHANGED
|
@@ -41,7 +41,7 @@ import { CancellableChildLifecycle, DEFAULT_CHILD_DEADLINE_MS } from "../core/ch
|
|
|
41
41
|
import { FailureSwarmRuntime } from "../core/failure-swarm-runtime.js";
|
|
42
42
|
import { EvidenceCapsuleStore } from "../core/evidence-capsule.js";
|
|
43
43
|
import { RunFlightLedger, diagnosisContractHash } from "../core/run-flight-ledger.js";
|
|
44
|
-
import { GOAL_BOUND_METADATA_KEY, goalFingerprint, selectGoalDelivery, validGoalEvidence } from "../core/goal-bound.js";
|
|
44
|
+
import { GOAL_BOUND_METADATA_KEY, goalFingerprint, goalValidationCommand, selectGoalDelivery, validGoalEvidence } from "../core/goal-bound.js";
|
|
45
45
|
import { LUNA_FABRIC_MAX_ACTIVE } from "../core/luna-fabric-scheduler.js";
|
|
46
46
|
import { terminalRescueModel } from "./terminal-rescue-binding.js";
|
|
47
47
|
import { TerminalRescueRuntime } from "../core/terminal-rescue-runtime.js";
|
|
@@ -1381,7 +1381,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1381
1381
|
const rawCommand = args !== undefined && typeof args.command === "string" ? normalizeCommand(args.command) : undefined;
|
|
1382
1382
|
if (rawCommand === undefined || rawCommand.length === 0)
|
|
1383
1383
|
return;
|
|
1384
|
-
const correcting = !!direct || await input.runtimeBridge?.ownsReviewerCorrection?.(toolInput.sessionID) === true
|
|
1384
|
+
const correcting = !!direct || await input.runtimeBridge?.ownsReviewerCorrection?.(toolInput.sessionID) === true ||
|
|
1385
|
+
await input.runtimeBridge?.ownsValidationRegistration?.(toolInput.sessionID) === true;
|
|
1385
1386
|
const directory = dockerRequest?.cwd ?? resolve(input.directory, typeof args?.workdir === "string" ? args.workdir : ".");
|
|
1386
1387
|
// A check is proof only in its registered cwd. Snapshot the same admitted source
|
|
1387
1388
|
// recipe at actual native start; never retrofit it to older external-cwd history.
|
|
@@ -1413,7 +1414,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1413
1414
|
let reusedEvidence;
|
|
1414
1415
|
const unitID = authorization.taskID;
|
|
1415
1416
|
const repairResume = operatorContractRepairResumes.get(root);
|
|
1416
|
-
const criteria = goal.acceptance_contract?.criteria.filter((criterion) => (correction ? criterion
|
|
1417
|
+
const criteria = goal.acceptance_contract?.criteria.filter((criterion) => (correction ? goalValidationCommand(goal, criterion) !== undefined && correction.commands.includes(goalValidationCommand(goal, criterion)) : goalValidationCommand(goal, criterion) === rawCommand) &&
|
|
1417
1418
|
criterion.expected_outcome === "pass") ?? [];
|
|
1418
1419
|
const denyValidation = (reason) => {
|
|
1419
1420
|
goalValidationDefects.add(toolInput.sessionID);
|
|
@@ -1438,7 +1439,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1438
1439
|
if (goal.goal_id !== null && criteria.length > 0) {
|
|
1439
1440
|
const requestedFull = criteria.some((criterion) => criterion.proof_scope === "requested-full");
|
|
1440
1441
|
const coordinatorOwned = requestedFull && unitID !== undefined &&
|
|
1441
|
-
await input.runtimeBridge?.ownsCanonicalValidation?.(root, unitID, toolInput.sessionID, correction ? criteria[0]
|
|
1442
|
+
await input.runtimeBridge?.ownsCanonicalValidation?.(root, unitID, toolInput.sessionID, correction ? goalValidationCommand(goal, criteria[0]) : rawCommand) === true;
|
|
1442
1443
|
if (requestedFull && !coordinatorOwned)
|
|
1443
1444
|
throw denyValidation("owner-mismatch: coordinator-routing-unavailable");
|
|
1444
1445
|
const profile = loaded?.validationProfile ?? DEFAULT_PLUGIN_OPTIONS.validationProfile;
|
|
@@ -2485,7 +2486,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2485
2486
|
Date.parse(execution.startedAt) >= Date.parse(check.startedAt));
|
|
2486
2487
|
return laterFailure ? [] : observedHostGoalEvidence({ ...check, owner: "coordinator", outcome: "pass",
|
|
2487
2488
|
immutableRef: goalFingerprint({ direct_check: check }),
|
|
2488
|
-
correction: { taskID: reservation.unitID, dispatchCallID: check.dispatchCallID,
|
|
2489
|
+
correction: { taskID: reservation.unitID, dispatchCallID: check.dispatchCallID,
|
|
2490
|
+
commands: check.matchedOccurrences.map(item => check.command[item.member]) } }, state, reservation.unitID);
|
|
2489
2491
|
});
|
|
2490
2492
|
const hostEvidence = [...new Map((directEvidence ?? [...observedEvidence, ...inherited]).map(entry => [entry.evidence_id, entry])).values()];
|
|
2491
2493
|
// A worker may return after a user scope epoch changed. Settle its spend/reservation, but only
|
|
@@ -8426,6 +8428,21 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8426
8428
|
sessionTaskIDs.set(child, taskID);
|
|
8427
8429
|
activateSession(child);
|
|
8428
8430
|
},
|
|
8431
|
+
registerDirectValidation: async (root, actor, criterionID, command, registeredAt) => {
|
|
8432
|
+
const direct = await input.runtimeBridge?.directMissionExecution?.(actor);
|
|
8433
|
+
if (!direct || direct.root !== root || !await input.runtimeBridge?.ownsCanonicalValidation?.(root, direct.taskID, actor, command)) {
|
|
8434
|
+
throw new Error("mission-validation-registration-owner-mismatch");
|
|
8435
|
+
}
|
|
8436
|
+
const ledger = await goalLedger(root), state = (await ledger.readGoal()).state;
|
|
8437
|
+
const reservation = state.outstanding_reservations.find(item => item.unit_id === direct.taskID && item.session_id === root);
|
|
8438
|
+
if (!reservation || state.phase !== "active" || !state.goal_id)
|
|
8439
|
+
throw new Error("mission-validation-registration-reservation-missing");
|
|
8440
|
+
const prior = state.validation_registrations?.[criterionID];
|
|
8441
|
+
if (prior?.unit_id === direct.taskID && prior.command === command && prior.registered_at === registeredAt)
|
|
8442
|
+
return;
|
|
8443
|
+
await ledger.appendGoal({ kind: "goal.validation-registered", at: new Date().toISOString(), goal_id: state.goal_id,
|
|
8444
|
+
reservation_id: reservation.reservation_id, unit_id: direct.taskID, criterion_id: criterionID, command, registered_at: registeredAt });
|
|
8445
|
+
},
|
|
8429
8446
|
activateDirectUnit: (root, actor, prompt, handoffPath) => serializeChatTransition(actor, async () => {
|
|
8430
8447
|
const direct = await input.runtimeBridge?.directMissionExecution?.(actor);
|
|
8431
8448
|
if (!direct || direct.root !== root)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type OperatorState } from "../core/operator-runtime.js";
|
|
2
2
|
import { type MissionEvidenceExcerpt, type MissionReviewScope, type OperatorMission } from "../core/operator-mission.js";
|
|
3
3
|
import { type RuntimeProfile } from "../core/runtime-profile.js";
|
|
4
|
-
import type { ReviewerCorrectionCheck } from "./runtime-bridge.js";
|
|
4
|
+
import type { DirectHandoffValidationCheck, ReviewerCorrectionCheck } from "./runtime-bridge.js";
|
|
5
5
|
export declare const validationDirectoryMatches: (commands: readonly string[], actual: string | undefined, directory?: string, directories?: Readonly<Record<string, string>>) => boolean;
|
|
6
6
|
/** Reviewer-facing evidence preserves execution/coverage; the host retains the full snapshot recipe. */
|
|
7
7
|
export declare function missionReviewValidation(run: OperatorState, statePath: string): {
|
|
@@ -125,7 +125,7 @@ export declare function reviewerCorrectionValidationFresh(validation: readonly s
|
|
|
125
125
|
nextOccurrence?: number;
|
|
126
126
|
}>;
|
|
127
127
|
/** Preserve a current ordered prefix while the next author executes the remaining recipe. */
|
|
128
|
-
export declare function directHandoffValidationChecks(...args: Parameters<typeof reviewerCorrectionValidationFresh>): Promise<readonly
|
|
128
|
+
export declare function directHandoffValidationChecks(...args: Parameters<typeof reviewerCorrectionValidationFresh>): Promise<readonly DirectHandoffValidationCheck[]>;
|
|
129
129
|
/** Summary-only native reader: unavailable API/error is not a successfully observed empty history. */
|
|
130
130
|
export declare function observedMissionValidationSummary(validation: readonly string[], childSessionID: string, read?: () => Promise<unknown>, notBefore?: number, directory?: string, directories?: Readonly<Record<string, string>>): Promise<Record<string, unknown>>;
|
|
131
131
|
export declare function missionReviewBaseline(directory: string): Promise<string | undefined>;
|
|
@@ -5,6 +5,7 @@ import { lstat, readlink, readdir } from "node:fs/promises";
|
|
|
5
5
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
|
+
import { unitValidation, unitValidationCwd } from "../core/operator-runtime.js";
|
|
8
9
|
import { TOOL_ENVIRONMENT } from "../runtime-mission-assets.js";
|
|
9
10
|
import { MISSION_REVIEW_REFERENCE, missionReviewScope } from "../core/operator-mission.js";
|
|
10
11
|
import { canonicalAgent } from "../core/runtime-profile.js";
|
|
@@ -39,9 +40,9 @@ export const validationDirectoryMatches = (commands, actual, directory, director
|
|
|
39
40
|
export function missionReviewValidation(run, statePath) {
|
|
40
41
|
return run.units.map(unit => ({
|
|
41
42
|
unit_id: unit.unit.id,
|
|
42
|
-
command: unit
|
|
43
|
-
...(unit
|
|
44
|
-
validation_cwd: unit
|
|
43
|
+
command: unitValidation(unit),
|
|
44
|
+
...(unitValidationCwd(unit) ? {
|
|
45
|
+
validation_cwd: unitValidationCwd(unit),
|
|
45
46
|
} : {}),
|
|
46
47
|
evidence: unit.evidence.map(({ protected_binding, ...evidence }) => ({
|
|
47
48
|
...evidence,
|
|
@@ -381,7 +382,11 @@ export async function directHandoffValidationChecks(...args) {
|
|
|
381
382
|
for (let count = validation.length; count > 0; count--) {
|
|
382
383
|
const result = await reviewerCorrectionValidationFresh(validation.slice(0, count), child, history, started, checks, root, directories, registeredAt);
|
|
383
384
|
if (result.ready)
|
|
384
|
-
return checks.
|
|
385
|
+
return checks.flatMap(check => {
|
|
386
|
+
const matchedOccurrences = result.matched?.filter(item => item.callID === check.callID)
|
|
387
|
+
.map(({ member, occurrence }) => ({ member, occurrence })) ?? [];
|
|
388
|
+
return matchedOccurrences.length ? [{ ...check, matchedOccurrences }] : [];
|
|
389
|
+
});
|
|
385
390
|
}
|
|
386
391
|
return [];
|
|
387
392
|
}
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { V010_RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
2
2
|
import { MISSION_BEHAVIOR_REVIEW, MISSION_GIT_SCOPE, REVIEWER_VALIDATION_WORKFLOW } from "../runtime-mission-assets.js";
|
|
3
|
-
import { cancelledMissionRetainsAcceptance, operatorGitPathAuthorized, OperatorContractError, OperatorRuntime } from "../core/operator-runtime.js";
|
|
3
|
+
import { cancelledMissionRetainsAcceptance, directValidationRegistrationPlanHash, unitValidation, unitValidationCwd, operatorGitPathAuthorized, OperatorContractError, OperatorRuntime } from "../core/operator-runtime.js";
|
|
4
4
|
import { DEFAULT_OPERATOR_PROPOSAL_BUDGET, OPERATOR_APPROVAL_CONTRACT, OPERATOR_PROPOSAL_BUDGET_CAPS, OPERATOR_PROPOSAL_REVISION_CONTRACT, OperatorProposalBudgetError, OperatorProposalRuntime } from "../core/operator-proposal.js";
|
|
5
5
|
import { CANONICAL_AGENT_ROLES, canonicalAgent, profileAgent, profileTool, V010_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
6
6
|
import { SortieDogsPlugin as canonicalPlugin, realAttachmentParts } from "./index.js";
|
|
@@ -256,7 +256,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
256
256
|
const method = typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages";
|
|
257
257
|
return observedMissionValidationSummary(validation, child, typeof nativeSession?.[method] === "function"
|
|
258
258
|
? () => session(method, { path: { id: child }, query: { directory: input.directory,
|
|
259
|
-
...(notBefore === undefined ? {} : { notBefore: new Date(notBefore).toISOString() }) } }) : undefined, notBefore, notBefore === undefined ? undefined : input.directory, direct
|
|
259
|
+
...(notBefore === undefined ? {} : { notBefore: new Date(notBefore).toISOString() }) } }) : undefined, notBefore, notBefore === undefined ? undefined : input.directory, direct ? unitValidationCwd(direct.unit) : undefined);
|
|
260
260
|
}
|
|
261
261
|
async function reviewMessages(id, notBefore, requireAvailable = false) {
|
|
262
262
|
const method = typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages";
|
|
@@ -498,6 +498,34 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
498
498
|
return undefined;
|
|
499
499
|
return unit ? { root, mission, run, unit } : undefined;
|
|
500
500
|
}
|
|
501
|
+
async function registeredValidation(id) {
|
|
502
|
+
const root = await rootFor(id);
|
|
503
|
+
if (!root)
|
|
504
|
+
return undefined;
|
|
505
|
+
const mission = await missions.read(root), run = await operators.read(root);
|
|
506
|
+
if (!mission || !run || mission.runID !== run.runID || run.phase !== "running" ||
|
|
507
|
+
["completed", "cancelled"].includes(mission.phase))
|
|
508
|
+
return undefined;
|
|
509
|
+
const unit = run.units.find(item => item.status === "running" && item.childSessionID === id && item.validationRegistration);
|
|
510
|
+
return unit ? { root, mission, run, unit } : undefined;
|
|
511
|
+
}
|
|
512
|
+
async function remainingRegisteredValidation(root, unit) {
|
|
513
|
+
const validation = unitValidation(unit);
|
|
514
|
+
if (unit.directExecution || !unit.validationRegistration || !unit.callID)
|
|
515
|
+
return validation;
|
|
516
|
+
const taskID = /^task_id: (.+)$/mu.exec(unit.task.prompt)?.[1];
|
|
517
|
+
const inherited = taskID ? await runtimeBridge.missionHandoffChecks(root, taskID, unit.callID) : [];
|
|
518
|
+
let prefix = inherited.reduce((count, check) => count + check.matchedOccurrences.length, 0);
|
|
519
|
+
// A new author's actual retry supersedes that inherited occurrence, including
|
|
520
|
+
// a later failure. Never let an earlier handed-off PASS hide this native result.
|
|
521
|
+
for (const check of unit.validationRegistration.checks)
|
|
522
|
+
for (const command of check.command) {
|
|
523
|
+
const occurrence = validation.indexOf(command);
|
|
524
|
+
if (occurrence >= 0)
|
|
525
|
+
prefix = Math.min(prefix, occurrence);
|
|
526
|
+
}
|
|
527
|
+
return validation.slice(prefix);
|
|
528
|
+
}
|
|
501
529
|
/**
|
|
502
530
|
* Resolve the owning root of a proposal investigation child for prompt assembly only.
|
|
503
531
|
*
|
|
@@ -644,14 +672,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
644
672
|
const unit = state.units.find(candidate => /^task_id: (.+)$/m.exec(candidate.task.prompt)?.[1] === taskID);
|
|
645
673
|
const active = unit?.status === "running" || (unit?.status === "failed" && unit.repairValidation !== null);
|
|
646
674
|
return active && unit.childSessionID === child &&
|
|
647
|
-
unit.
|
|
675
|
+
unitValidation(unit).some(candidate => normalizeCommand(candidate) === command);
|
|
648
676
|
},
|
|
649
677
|
beforeValidationSnapshot: (root, child, command) => operators.beforePostCommitValidation(root, child, command),
|
|
650
678
|
requiresValidationExecution: async (root, taskID, child, commands) => {
|
|
651
679
|
const run = await operators.read(root);
|
|
652
680
|
const unit = run?.units.find(item => item.status === "running" && item.childSessionID === child &&
|
|
653
681
|
/^task_id: (.+)$/mu.exec(item.task.prompt)?.[1] === taskID);
|
|
654
|
-
return !!unit && commands.some(command => unit.
|
|
682
|
+
return !!unit && commands.some(command => unitValidation(unit).filter(item => normalizeCommand(item) === command).length > 1);
|
|
655
683
|
},
|
|
656
684
|
ownsMissionDispatch: async (root, callID, taskID) => {
|
|
657
685
|
const mission = await missions.read(root), state = await operators.read(root);
|
|
@@ -661,6 +689,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
661
689
|
/^task_id: (.+)$/m.exec(unit.task.prompt)?.[1] === taskID);
|
|
662
690
|
},
|
|
663
691
|
ownsReviewerCorrection: async (child) => !!await reviewerCorrection(child),
|
|
692
|
+
ownsValidationRegistration: async (child) => !!await registeredValidation(child),
|
|
664
693
|
directMissionExecution: async (actor) => {
|
|
665
694
|
const direct = await directExecution(actor);
|
|
666
695
|
const taskID = direct && /^task_id: (.+)$/mu.exec(direct.unit.task.prompt)?.[1];
|
|
@@ -683,21 +712,24 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
683
712
|
return [];
|
|
684
713
|
try {
|
|
685
714
|
const history = await reviewMessages(prior.actor, prior.startedAt, true);
|
|
686
|
-
return await directHandoffValidationChecks(unit
|
|
715
|
+
return await directHandoffValidationChecks(unitValidation(unit), prior.actor, history, Date.parse(prior.startedAt), prior.checks, input.directory, prior.validationCwd ?? unit.unit.validation_cwd, prior.validationRegisteredAt);
|
|
687
716
|
}
|
|
688
717
|
catch {
|
|
689
718
|
return [];
|
|
690
719
|
}
|
|
691
720
|
},
|
|
692
721
|
reviewerCorrectionValidationMembers: async (child, command, directory) => {
|
|
693
|
-
const correction = await reviewerCorrection(child) ?? await directExecution(child);
|
|
722
|
+
const correction = await reviewerCorrection(child) ?? await directExecution(child) ?? await registeredValidation(child);
|
|
694
723
|
if (!correction)
|
|
695
724
|
return undefined;
|
|
696
725
|
const unit = correction.unit;
|
|
697
|
-
const directories = unit
|
|
726
|
+
const directories = unitValidationCwd(unit);
|
|
698
727
|
const admittedAt = unit.reviewerCorrection?.admittedAt ?? unit.directExecution?.startedAt ?? correction.run.createdAt;
|
|
699
|
-
const
|
|
700
|
-
const
|
|
728
|
+
const validation = await remainingRegisteredValidation(correction.root, unit);
|
|
729
|
+
const progress = reviewerCorrectionValidation(validation, child, await reviewMessages(child, admittedAt), Date.parse(admittedAt), unit.reviewerCorrection?.checks ?? unit.directExecution?.checks ?? unit.validationRegistration?.checks ?? [], input.directory, directories, unit.reviewerCorrection?.validationRegisteredAt ?? unit.directExecution?.validationRegisteredAt ?? unit.validationRegistration?.registeredAt);
|
|
730
|
+
// Progress is relative to the remaining suffix; native matching uses the
|
|
731
|
+
// full declaration so a retry of an inherited occurrence is captured too.
|
|
732
|
+
const members = canonicalObservedValidationMembers(command, unitValidation(unit), unitValidation(unit).length - validation.length + (progress.nextOccurrence ?? 0));
|
|
701
733
|
return members && validationDirectoryMatches(members, directory, input.directory, directories) ? members : undefined;
|
|
702
734
|
},
|
|
703
735
|
recordReviewerCorrectionCheck: (root, taskID, check) => operators.recordReviewerCorrectionCheck(root, taskID, check),
|
|
@@ -706,18 +738,21 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
706
738
|
// Recovery also compares failed units after their native Task has ended.
|
|
707
739
|
const unit = run?.units.find(item => item.childSessionID === child &&
|
|
708
740
|
/^task_id: (.+)$/mu.exec(item.task.prompt)?.[1] === taskID);
|
|
709
|
-
return !unit || validationDirectoryMatches(commands, directory, input.directory, unit
|
|
741
|
+
return !unit || validationDirectoryMatches(commands, directory, input.directory, unitValidationCwd(unit));
|
|
710
742
|
},
|
|
711
743
|
reviewerCorrectionValidation: async (root, callID, child, startedAt) => {
|
|
712
744
|
const run = await operators.read(root);
|
|
713
745
|
const unit = run?.units.find(item => item.callID === callID && item.childSessionID === child &&
|
|
714
|
-
(item.reviewerCorrection?.author === child || item.directExecution?.actor === child));
|
|
746
|
+
(item.reviewerCorrection?.author === child || item.directExecution?.actor === child || item.validationRegistration));
|
|
715
747
|
if (!unit)
|
|
716
748
|
return undefined;
|
|
717
749
|
// Compaction removes earlier tools from session.context, not from native history.
|
|
718
750
|
// Reuse the admission-window reader; saved checks still require their live input binding.
|
|
719
751
|
try {
|
|
720
|
-
|
|
752
|
+
const validation = await remainingRegisteredValidation(root, unit);
|
|
753
|
+
if (!validation.length && unitValidation(unit).length)
|
|
754
|
+
return { ready: true, matched: [] };
|
|
755
|
+
return await reviewerCorrectionValidationFresh(validation, child, await reviewMessages(child, new Date(startedAt).toISOString()), startedAt, unit.reviewerCorrection?.checks ?? unit.directExecution?.checks ?? unit.validationRegistration?.checks ?? [], input.directory, unitValidationCwd(unit), unit.reviewerCorrection?.validationRegisteredAt ?? unit.directExecution?.validationRegisteredAt ?? unit.validationRegistration?.registeredAt);
|
|
721
756
|
}
|
|
722
757
|
catch {
|
|
723
758
|
return { ready: false, reason: "mission-review-correction-validation-history-unavailable" };
|
|
@@ -1216,6 +1251,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1216
1251
|
return packet;
|
|
1217
1252
|
}
|
|
1218
1253
|
function missionDispatchPacket(mission, run) {
|
|
1254
|
+
const strategyRecovery = " If a method stalled or proposed avoidable extra work, Operator: compare the original goal, actual result and concrete unmet work before choosing to continue, change or stop that method. " +
|
|
1255
|
+
`For a concrete adjustment, use ${profile.toolPrefix}plan_units with proposed units, reason and handoff_context, then dispatch its returned same-Coordinator continuation. ` +
|
|
1256
|
+
"Preserve required checks through lawful registration/replanning and reuse current evidence. Otherwise continue normally; no extra approval or logging-only step.";
|
|
1219
1257
|
const packet = { ...missionPacket(mission, run), project_root: input.directory,
|
|
1220
1258
|
coordinator_dispatch: mission.dispatchOpen ? "active" : ["completed", "cancelled"].includes(mission.phase) ? "terminal" : "resumable" };
|
|
1221
1259
|
if (mission.phase === "submitted" && mission.submission?.status === "blocked" &&
|
|
@@ -1249,7 +1287,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1249
1287
|
if (mission.runID === run?.runID && run?.units.some(unit => unit.status === "running")) {
|
|
1250
1288
|
if (mission.coordinator && !mission.dispatchOpen && !["completed", "cancelled"].includes(mission.phase))
|
|
1251
1289
|
return { ...packet, task: missions.task(mission),
|
|
1252
|
-
next_action: "Resume this existing Coordinator Task with its task_id to continue the retained unit. Do not dispatch a replacement or wait for background work that has already ended." };
|
|
1290
|
+
next_action: "Resume this existing Coordinator Task with its task_id to continue the retained unit. Do not dispatch a replacement or wait for background work that has already ended." + strategyRecovery };
|
|
1253
1291
|
return { ...packet, next_action: "The current unit is active. Continue its owning session; do not dispatch a replacement Worker or Coordinator. Native background completion will return to the parent." };
|
|
1254
1292
|
}
|
|
1255
1293
|
if (mission.coordinator === null && mission.runID === run?.runID && !mission.dispatchOpen &&
|
|
@@ -1295,7 +1333,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1295
1333
|
next_action: (run?.units.some(unit => unit.dispatchDenial) ? `${packet.next_action}\n` : "") +
|
|
1296
1334
|
(mission.coordinator === null
|
|
1297
1335
|
? "Fast-lane needs correction or coordination: dispatch this exact Coordinator Task with the existing Worker changes, checks and concrete remaining work. Keep the same mission, requirements and cumulative budget; do not replace an active Worker."
|
|
1298
|
-
: "The previous Coordinator Task is finished. Dispatch this exact Task to continue the same mission and Coordinator session; keep the original requirements and cumulative budget.") };
|
|
1336
|
+
: "The previous Coordinator Task is finished. Dispatch this exact Task to continue the same mission and Coordinator session; keep the original requirements and cumulative budget." + strategyRecovery) };
|
|
1299
1337
|
}
|
|
1300
1338
|
return packet;
|
|
1301
1339
|
}
|
|
@@ -1524,6 +1562,58 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1524
1562
|
await operators.recordStoppedChild(root, run.runID, child);
|
|
1525
1563
|
}
|
|
1526
1564
|
}
|
|
1565
|
+
async function cancelledUnclaimedTaskCalls(root, run) {
|
|
1566
|
+
const units = run.units.filter(unit => !unit.directExecution && unit.status === "cancelled" &&
|
|
1567
|
+
unit.childSessionID === null && unit.callID !== null);
|
|
1568
|
+
if (!units.length)
|
|
1569
|
+
return undefined;
|
|
1570
|
+
const owner = run.operatorSessionID ?? root;
|
|
1571
|
+
const deny = () => new Error("mission-cancelled-run-task-not-terminal");
|
|
1572
|
+
// The bounded chat context may omit the old Task. Use the existing native
|
|
1573
|
+
// history reader, and never substitute a model's stop report for a tool terminal.
|
|
1574
|
+
const history = await reviewMessages(owner, undefined, true);
|
|
1575
|
+
for (const unit of units) {
|
|
1576
|
+
const parts = history.flatMap(message => record(message.info) && message.info.role === "assistant" &&
|
|
1577
|
+
message.info.sessionID === owner && Array.isArray(message.parts) ? message.parts : []).filter(part => record(part) && part.type === "tool" && part.tool === "task" && part.callID === unit.callID);
|
|
1578
|
+
const part = parts.length === 1 && record(parts[0]) ? parts[0] : undefined;
|
|
1579
|
+
const state = part && record(part.state) ? part.state : undefined;
|
|
1580
|
+
if (!part || part.sessionID !== undefined && part.sessionID !== owner || !state ||
|
|
1581
|
+
!["completed", "error"].includes(String(state.status)) ||
|
|
1582
|
+
record(state.metadata) && ["running", "pending", "unknown"].includes(String(state.metadata.status)) ||
|
|
1583
|
+
!record(state.time) || typeof state.time.end !== "number" || !Number.isFinite(state.time.end) ||
|
|
1584
|
+
!operators.matchesCancelledWorkerTask(run, unit.unit.id, state.input) || taskChildSessionID(state) !== undefined)
|
|
1585
|
+
throw deny();
|
|
1586
|
+
}
|
|
1587
|
+
// A parent Task terminal does not prove that an unclaimed child never started.
|
|
1588
|
+
// Reuse the owning service's active snapshot and child lineage before allowing
|
|
1589
|
+
// this formerly unrepresentable cancellation into prepareMission.
|
|
1590
|
+
const active = payload(await session("active", {}));
|
|
1591
|
+
if (!record(active) || !Object.values(active).every(value => record(value) &&
|
|
1592
|
+
["running", "retry"].includes(String(value.type))) || owner !== root && Object.hasOwn(active, owner))
|
|
1593
|
+
throw deny();
|
|
1594
|
+
const children = payload(await session("children", { path: { id: owner }, query: { directory: input.directory } }));
|
|
1595
|
+
if (!Array.isArray(children) || children.some(child => !record(child) || typeof child.id !== "string") ||
|
|
1596
|
+
new Set(children.map(child => child.id)).size !== children.length)
|
|
1597
|
+
throw deny();
|
|
1598
|
+
for (const child of children) {
|
|
1599
|
+
const info = payload(await session("get", { path: { id: child.id }, query: { directory: input.directory } }));
|
|
1600
|
+
if (!record(info) || info.id !== child.id || info.parentID !== owner ||
|
|
1601
|
+
typeof info.agent !== "string" || !info.agent.trim())
|
|
1602
|
+
throw deny();
|
|
1603
|
+
const role = canonicalAgent(profile, info.agent);
|
|
1604
|
+
// Root Fast-lane Tasks must not claim unrelated profile or native agents.
|
|
1605
|
+
if (owner === root && role !== "dog-worker" && role !== "dog-luna-worker")
|
|
1606
|
+
continue;
|
|
1607
|
+
if (!role)
|
|
1608
|
+
throw deny();
|
|
1609
|
+
const descendants = payload(await session("children", { path: { id: child.id }, query: { directory: input.directory } }));
|
|
1610
|
+
if (Object.hasOwn(active, child.id) || !Array.isArray(descendants) || descendants.length ||
|
|
1611
|
+
!["succeeded", "failed", "interrupted"].includes(String(info.outcome)) &&
|
|
1612
|
+
!(info.outcome === undefined && run.stoppedChildren?.includes(child.id)))
|
|
1613
|
+
throw deny();
|
|
1614
|
+
}
|
|
1615
|
+
return { runID: run.runID, callIDs: units.map(unit => unit.callID) };
|
|
1616
|
+
}
|
|
1527
1617
|
async function stop(root, reason, retireRoot = true) {
|
|
1528
1618
|
const directActor = (await operators.read(root))?.units.find(unit => unit.status === "running" && unit.directExecution)?.directExecution?.actor;
|
|
1529
1619
|
// Stopping owned execution does not write its artifacts. Its existing reservation
|
|
@@ -1920,7 +2010,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1920
2010
|
const gapSummary = reviewGaps?.replace(/^EVIDENCE_GAPS\s*/u, "").replace(/\s+/gu, " ").slice(0, 500);
|
|
1921
2011
|
const text = withMissionOperationOutcome(`✅ **DONE** \`${state.runID}\` — declared checks passed; Operator accepted the result.`, mission) + `\n\n` +
|
|
1922
2012
|
`**${t("変更点", "Changes")}:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
|
|
1923
|
-
`**${t("確認結果", "Validation")}:** ${t("宣言検証合格", "Declared checks passed")} — ${[...new Set(state.units.flatMap(
|
|
2013
|
+
`**${t("確認結果", "Validation")}:** ${t("宣言検証合格", "Declared checks passed")} — ${[...new Set(state.units.flatMap(unitValidation))].join("; ")}` +
|
|
1924
2014
|
(mission?.review ? `\n${t("レビュー", "Review")}: ${mission.review.verdict === "self-rechecked" ? t("修正著者の自己再確認(独立PASSではない)", "Author self-rechecked (not independent PASS)") : mission.review.verdict === "evidence-gaps" ? t("証拠不足を残して受入れ(レビューPASSではない)", "Accepted with evidence gaps (not Review PASS)") : mission.review.verdict}.` : "") +
|
|
1925
2015
|
(mission?.deliveryObservation?.head ? `\nCommit: ${mission.deliveryObservation.head} (${mission.deliveryObservation.branch ?? "detached"}; clean=${mission.deliveryObservation.clean})` : "") +
|
|
1926
2016
|
(reviewGaps ? `\n\n**${t("レビュー補足", "Review note")}:** ${gapSummary}\n\n**${t("次", "Next")}:** ${t("なし(補足だけを理由に再レビュー不要)", "None (no repeat review solely for the advisory)")}`
|
|
@@ -2008,8 +2098,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2008
2098
|
recovered.set(unit.unit.id, await control.recoverUnitEvidence(context.sessionID, { unitID: taskID,
|
|
2009
2099
|
childSessionID: direct.actor, manifestPath: unit.manifestPath, manifestHash: unit.hashes[1], goalFingerprint,
|
|
2010
2100
|
direct: { callID: unit.callID, startedAt: direct.startedAt, finishedAt: direct.finishedAt,
|
|
2011
|
-
checks: unit.reviewerCorrection?.checks ?? direct.checks, validation: unit
|
|
2012
|
-
directories:
|
|
2101
|
+
checks: unit.reviewerCorrection?.checks ?? direct.checks, validation: unitValidation(unit),
|
|
2102
|
+
directories: unitValidationCwd(unit),
|
|
2013
2103
|
registeredAt: unit.reviewerCorrection?.validationRegisteredAt ?? direct.validationRegisteredAt } }));
|
|
2014
2104
|
recoveredDirect.add(unit.unit.id);
|
|
2015
2105
|
continue;
|
|
@@ -2284,9 +2374,18 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2284
2374
|
} };
|
|
2285
2375
|
const startMission = `${profile.toolPrefix}start_mission`, planUnits = `${profile.toolPrefix}plan_units`, reviewMission = `${profile.toolPrefix}review_mission`, repairReview = `${profile.toolPrefix}repair_review`, submitMission = `${profile.toolPrefix}submit_mission`, completeMission = `${profile.toolPrefix}complete_mission`, expandUnit = `${profile.toolPrefix}expand_unit`, skipMissionConsultation = `${profile.toolPrefix}skip_mission_consultation`, extendMissionBudget = `${profile.toolPrefix}extend_mission_budget`;
|
|
2286
2376
|
const startDirectUnit = `${profile.toolPrefix}start_direct_unit`, finishDirectUnit = `${profile.toolPrefix}finish_direct_unit`;
|
|
2377
|
+
async function syncDirectValidation(direct) {
|
|
2378
|
+
if (!direct.unit.validationRegistration)
|
|
2379
|
+
return;
|
|
2380
|
+
const last = unitValidation(direct.unit).at(-1);
|
|
2381
|
+
await control.registerDirectValidation(direct.root, direct.unit.directExecution.actor, direct.unit.unit.id, last, direct.unit.directExecution.validationRegisteredAt?.[last] ?? direct.unit.directExecution.startedAt);
|
|
2382
|
+
}
|
|
2287
2383
|
async function activateDirect(actor) {
|
|
2288
2384
|
const direct = await directExecution(actor);
|
|
2289
2385
|
if (direct) {
|
|
2386
|
+
// Recover a crash between durable registration and its goal-ledger projection
|
|
2387
|
+
// before admitting another native check or selecting finish evidence.
|
|
2388
|
+
await syncDirectValidation(direct);
|
|
2290
2389
|
await control.activateDirectUnit(direct.root, actor, direct.unit.task.prompt, direct.unit.handoffPath);
|
|
2291
2390
|
}
|
|
2292
2391
|
return direct;
|
|
@@ -2308,7 +2407,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2308
2407
|
throw new Error("mission-direct-binding-unavailable: retain the admitted unit and restore its Mission binding");
|
|
2309
2408
|
await activateDirect(actor);
|
|
2310
2409
|
return JSON.stringify({ status: "direct-unit-running", run_id: direct.run.runID, unit_id: direct.unit.unit.id,
|
|
2311
|
-
executor_session_id: actor, validation: direct.unit
|
|
2410
|
+
executor_session_id: actor, validation: unitValidation(direct.unit),
|
|
2312
2411
|
next_action: `Implement/correct and run the declared checks here, in this same session. Call ${finishDirectUnit} after the requested delivery boundary. No Worker or handoff read is needed. Retain the original requirements; independent Review and Operator acceptance still follow.` });
|
|
2313
2412
|
}
|
|
2314
2413
|
async function assertMissionRunBound(root) {
|
|
@@ -2317,10 +2416,45 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2317
2416
|
throw new Error("mission-run-registration-incomplete: retry plan_units with the same plan to finish goal registration and Mission binding; no unit admitted");
|
|
2318
2417
|
await operators.verifyContinuityControls(run);
|
|
2319
2418
|
}
|
|
2419
|
+
async function assertDirectPlanningQuiescent(actor, planningCallID) {
|
|
2420
|
+
await control.assertDirectUnitQuiescent(actor);
|
|
2421
|
+
// Cold hook state is not proof. Only the exact current planner may be pending;
|
|
2422
|
+
// a completed background launch or another Task remains an active/unproven writer.
|
|
2423
|
+
const history = payload(await session("messages", { path: { id: actor }, query: { directory: input.directory } }));
|
|
2424
|
+
if (!Array.isArray(history))
|
|
2425
|
+
throw new Error("mission-replan-terminal-unreconciled:tool_terminal_records_unavailable");
|
|
2426
|
+
const calls = new Map();
|
|
2427
|
+
for (const message of history) {
|
|
2428
|
+
if (!record(message) || !record(message.info) || message.info.sessionID !== actor || !Array.isArray(message.parts)) {
|
|
2429
|
+
throw new Error("mission-replan-terminal-unreconciled:tool_terminal_records_unavailable");
|
|
2430
|
+
}
|
|
2431
|
+
for (const part of message.parts) {
|
|
2432
|
+
if (!record(part) || typeof part.type !== "string")
|
|
2433
|
+
throw new Error("mission-replan-terminal-unreconciled:tool_terminal_records_unavailable");
|
|
2434
|
+
if (part.type !== "tool")
|
|
2435
|
+
continue;
|
|
2436
|
+
if (part.callID === planningCallID && planningCallID && part.tool === planUnits)
|
|
2437
|
+
continue;
|
|
2438
|
+
if (typeof part.callID !== "string")
|
|
2439
|
+
throw new Error("mission-replan-terminal-unreconciled:tool_identity_unavailable");
|
|
2440
|
+
const prior = calls.get(part.callID);
|
|
2441
|
+
if (prior && prior.tool !== part.tool)
|
|
2442
|
+
throw new Error("mission-replan-terminal-unreconciled:tool_identity_conflict");
|
|
2443
|
+
calls.set(part.callID, part);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
for (const part of calls.values()) {
|
|
2447
|
+
if (!record(part.state) || !["completed", "error"].includes(String(part.state.status)) ||
|
|
2448
|
+
record(part.state.metadata) && ["running", "pending", "unknown"].includes(String(part.state.metadata.status))) {
|
|
2449
|
+
throw new Error("mission-replan-terminal-unreconciled:tool_dispatch_active_or_unproven");
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2320
2453
|
async function handoffDirect(root, actor, callID, notes) {
|
|
2321
2454
|
const direct = await directExecution(actor);
|
|
2322
2455
|
if (direct) {
|
|
2323
2456
|
await control.assertDirectUnitQuiescent(actor);
|
|
2457
|
+
await syncDirectValidation(direct);
|
|
2324
2458
|
const history = await reviewMessages(actor, direct.unit.directExecution.startedAt, true);
|
|
2325
2459
|
const calls = new Map();
|
|
2326
2460
|
for (const message of history)
|
|
@@ -2390,7 +2524,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2390
2524
|
}
|
|
2391
2525
|
if (direct.unit.reviewerCorrection)
|
|
2392
2526
|
return JSON.stringify({ status: "correction-validated",
|
|
2393
|
-
execution: "same-native-task", validation: direct.unit
|
|
2527
|
+
execution: "same-native-task", validation: unitValidation(direct.unit),
|
|
2394
2528
|
next_action: 'Compare the original requirements, retained findings and affected public behavior in this same context, then finish THIS native Task with SELF_RECHECKED and self_recheck: {"candidate":"current-validated","unresolved_findings":[],"residual_major":null}. No handoff reread, second Task or Operator round trip. This is author self-recheck; only the actual successful native terminal can bind it, never this tool result.' });
|
|
2395
2529
|
const run = await operators.required(root);
|
|
2396
2530
|
// Full protected path arrays can make V2 truncate this one-line JSON to zero lines,
|
|
@@ -2952,8 +3086,24 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2952
3086
|
// packet can already contain one even while its Reviewer is still pending.
|
|
2953
3087
|
const { task: _dispatchTask, ...packet } = missionDispatchPacket(mission, previous);
|
|
2954
3088
|
const reviewPending = mission.runID === previous?.runID && mission.review?.verdict === "pending";
|
|
2955
|
-
const
|
|
2956
|
-
|
|
3089
|
+
const running = mission.runID === previous?.runID ? previous.units.filter(unit => unit.status === "running") : [];
|
|
3090
|
+
let retainedDirectOwnerEnded = false;
|
|
3091
|
+
if (!mission.dispatchOpen && !reviewPending && mission.coordinator && running.length &&
|
|
3092
|
+
running.every(unit => unit.directExecution?.actor === mission.coordinator && !unit.reviewerCorrection)) {
|
|
3093
|
+
// This only forwards a proposal to the SAME owner. Native session termination
|
|
3094
|
+
// does not settle its retained unit or authorize a new writer/contract.
|
|
3095
|
+
const native = payload(await session("get", { path: { id: mission.coordinator } }).catch(() => undefined));
|
|
3096
|
+
if (record(native) && native.id === mission.coordinator && native.parentID === root &&
|
|
3097
|
+
canonicalAgent(profile, native.agent) === "dog-operator" &&
|
|
3098
|
+
["succeeded", "failed", "interrupted"].includes(String(native.outcome))) {
|
|
3099
|
+
try {
|
|
3100
|
+
await assertDirectPlanningQuiescent(mission.coordinator);
|
|
3101
|
+
retainedDirectOwnerEnded = true;
|
|
3102
|
+
}
|
|
3103
|
+
catch { /* A live/unproven native tool still owns this work; do not offer another Task. */ }
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
const active = mission.dispatchOpen || reviewPending || running.length > 0 && !retainedDirectOwnerEnded;
|
|
2957
3107
|
const continuation = !active ? missions.task(mission) : undefined;
|
|
2958
3108
|
// Existing Coordinator resumes accept a concrete prompt. Do not use the opaque
|
|
2959
3109
|
// initial reference here: claim() would expand it from unchanged Mission state
|
|
@@ -2975,25 +3125,23 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2975
3125
|
const same = previous?.planHash === createHash("sha256").update(JSON.stringify(plan)).digest("hex");
|
|
2976
3126
|
const replanning = previous && !["completed", "cancelled"].includes(previous.phase) && !same;
|
|
2977
3127
|
const activeDirect = previous?.units.find(unit => unit.status === "running" && unit.directExecution?.actor === actor);
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
if (prior?.validation_cwd)
|
|
2984
|
-
return { ...unit, validation_cwd: prior.validation_cwd };
|
|
2985
|
-
const { validation_cwd: _cwd, ...rest } = unit;
|
|
2986
|
-
return rest;
|
|
2987
|
-
}) })).digest("hex");
|
|
3128
|
+
const nextRegistration = plan.units.find(unit => unit.id === activeDirect?.unit.id);
|
|
3129
|
+
const registrationChanged = activeDirect && (JSON.stringify(unitValidationCwd(activeDirect) ?? {}) !==
|
|
3130
|
+
JSON.stringify(nextRegistration?.validation_cwd ?? {}) ||
|
|
3131
|
+
JSON.stringify(unitValidation(activeDirect)) !== JSON.stringify(nextRegistration?.validation));
|
|
3132
|
+
const registrationPlanHash = previous && activeDirect && directValidationRegistrationPlanHash(previous, plan, activeDirect.unit.id);
|
|
2988
3133
|
if (previous && activeDirect && (registrationChanged || replanning && registrationPlanHash === previous.planHash)) {
|
|
2989
3134
|
if (!reason?.trim())
|
|
2990
3135
|
throw new Error("mission-replan-reason-required: name the validation registration correction");
|
|
2991
3136
|
if (execution !== undefined && !emptyImplementationExecution)
|
|
2992
3137
|
throw new Error("mission-validation-registration-operation-change-forbidden");
|
|
3138
|
+
await assertDirectPlanningQuiescent(actor, planningCallID);
|
|
2993
3139
|
const corrected = await operators.correctDirectValidationRegistration(root, actor, previous.runID, plan);
|
|
2994
3140
|
const unit = corrected.units.find(unit => unit.status === "running" && unit.directExecution?.actor === actor);
|
|
3141
|
+
const last = unitValidation(unit).at(-1);
|
|
3142
|
+
await control.registerDirectValidation(root, actor, unit.unit.id, last, unit.directExecution.validationRegisteredAt?.[last] ?? unit.directExecution.startedAt);
|
|
2995
3143
|
return JSON.stringify({ status: "direct-unit-registration-corrected", run_id: corrected.runID, unit_id: unit.unit.id,
|
|
2996
|
-
executor_session_id: actor, validation: unit
|
|
3144
|
+
executor_session_id: actor, validation: unitValidation(unit), validation_cwd: unitValidationCwd(unit),
|
|
2997
3145
|
registered_at: unit.directExecution.validationRegisteredAt,
|
|
2998
3146
|
next_action: `Run the affected declared checks in their registered cwd here, then ${finishDirectUnit}. Old observations remain historical; no new Task, terminal, reservation or benchmark attempt was created.` });
|
|
2999
3147
|
}
|
|
@@ -3019,38 +3167,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3019
3167
|
throw new Error(`mission-replan-terminal-unreconciled:${"reason" in terminal ? terminal.reason : "unknown"}`);
|
|
3020
3168
|
}
|
|
3021
3169
|
if (ownDirect) {
|
|
3022
|
-
|
|
3023
|
-
// a completed background launch still owns a running process. Only this exact planner
|
|
3024
|
-
// call may be pending while it ends its own admission.
|
|
3025
|
-
const history = payload(await session("messages", { path: { id: actor }, query: { directory: input.directory } }));
|
|
3026
|
-
if (!Array.isArray(history))
|
|
3027
|
-
throw new Error("mission-replan-terminal-unreconciled:tool_terminal_records_unavailable");
|
|
3028
|
-
const calls = new Map();
|
|
3029
|
-
for (const message of history) {
|
|
3030
|
-
if (!record(message) || !record(message.info) || message.info.sessionID !== actor || !Array.isArray(message.parts)) {
|
|
3031
|
-
throw new Error("mission-replan-terminal-unreconciled:tool_terminal_records_unavailable");
|
|
3032
|
-
}
|
|
3033
|
-
for (const part of message.parts) {
|
|
3034
|
-
if (!record(part) || typeof part.type !== "string")
|
|
3035
|
-
throw new Error("mission-replan-terminal-unreconciled:tool_terminal_records_unavailable");
|
|
3036
|
-
if (part.type !== "tool")
|
|
3037
|
-
continue;
|
|
3038
|
-
if (part.callID === planningCallID && planningCallID && part.tool === planUnits)
|
|
3039
|
-
continue;
|
|
3040
|
-
if (typeof part.callID !== "string")
|
|
3041
|
-
throw new Error("mission-replan-terminal-unreconciled:tool_identity_unavailable");
|
|
3042
|
-
const prior = calls.get(part.callID);
|
|
3043
|
-
if (prior && prior.tool !== part.tool)
|
|
3044
|
-
throw new Error("mission-replan-terminal-unreconciled:tool_identity_conflict");
|
|
3045
|
-
calls.set(part.callID, part);
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
for (const part of calls.values()) {
|
|
3049
|
-
if (!record(part.state) || !["completed", "error"].includes(String(part.state.status)) ||
|
|
3050
|
-
(record(part.state.metadata) && ["running", "pending", "unknown"].includes(String(part.state.metadata.status)))) {
|
|
3051
|
-
throw new Error("mission-replan-terminal-unreconciled:tool_dispatch_active_or_unproven");
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3170
|
+
await assertDirectPlanningQuiescent(actor, planningCallID);
|
|
3054
3171
|
await activateDirect(actor);
|
|
3055
3172
|
await control.finishDirectUnit(root, actor, []);
|
|
3056
3173
|
previous = await operators.required(root);
|
|
@@ -3062,7 +3179,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3062
3179
|
const cancelledPredecessor = previous?.phase === "cancelled" &&
|
|
3063
3180
|
(mission.supersededRunID === previous.runID ||
|
|
3064
3181
|
(mission.supersededRunID === undefined && ["explicit-cancellation", "agent-changed"].includes(previous.decision ?? "") &&
|
|
3065
|
-
previous.units.some(unit => (previous
|
|
3182
|
+
previous.units.some(unit => (previous?.decision === "agent-changed" || unit.status === "cancelled") &&
|
|
3183
|
+
(unit.childSessionID !== null || !unit.directExecution && unit.callID !== null))));
|
|
3066
3184
|
let budget = await control.currentBudget(root);
|
|
3067
3185
|
if (cancelledPredecessor && budget?.reserved_units) {
|
|
3068
3186
|
await control.reconcileAbortedOperatorOrphan(root);
|
|
@@ -3073,6 +3191,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3073
3191
|
// Reconcile it even when reservation recovery has nothing to do. Never resume it,
|
|
3074
3192
|
// interrupt live work here, or manufacture its native terminal/validation records.
|
|
3075
3193
|
await stopCancelledChildren(root, previous, true);
|
|
3194
|
+
previous = await operators.required(root);
|
|
3076
3195
|
}
|
|
3077
3196
|
if (budget && budget.remaining_units < plan.units.length && !same)
|
|
3078
3197
|
throw new Error(`mission-budget-exhausted: plan needs ${plan.units.length} units; ${budget.remaining_units} remain. ` +
|
|
@@ -3110,8 +3229,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3110
3229
|
...(previous ? { retained_validation: missionReviewValidation(previous, replanning || previous.phase === "completed"
|
|
3111
3230
|
? operators.replanArchivePath(previous) : cancelledPredecessor ? `${operators.statePath(root)}.${previous.runID}.archive` : operators.statePath(root)),
|
|
3112
3231
|
prior_run_id: previous.runID } : {}) };
|
|
3232
|
+
const terminalTaskCalls = cancelledPredecessor && previous ? await cancelledUnclaimedTaskCalls(root, previous) : undefined;
|
|
3113
3233
|
const state = replanning && previous ? await operators.replanMission(root, previous.runID, plan, dispatcher, context)
|
|
3114
|
-
: await operators.prepareMission(root, plan, dispatcher, mission.supersededRunID, terminalChildren, mission.requirementsReplaced, context);
|
|
3234
|
+
: await operators.prepareMission(root, plan, dispatcher, mission.supersededRunID, terminalChildren, mission.requirementsReplaced, context, terminalTaskCalls);
|
|
3115
3235
|
// prepareMission retains an identical prepared run after either registration stage
|
|
3116
3236
|
// fails. Verify its pinned controls, then idempotently register the same declaration;
|
|
3117
3237
|
// only the durable Mission binding below makes it eligible for admission.
|
|
@@ -3151,7 +3271,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3151
3271
|
handoff_context: handoffContextSchema,
|
|
3152
3272
|
reason: { type: "string", "x-sortie-optional": true },
|
|
3153
3273
|
purpose_decision: purposeDecisionSchema,
|
|
3154
|
-
executor: { type: "string", enum: ["worker", "self"], description: "Use self to implement/correct and formally validate here. Use worker
|
|
3274
|
+
executor: { type: "string", enum: ["worker", "self"], description: "Use self to implement/correct and formally validate here. Use executor=worker with units=\"\" in V2 (omit units in legacy; never units=[]) to yield the current direct unit, carrying remaining action in handoff_context; the same unit, edits, checks and budget continue. With units, worker/default retains configured Worker routing.", "x-sortie-optional": true } }, execute: async (args, context) => {
|
|
3155
3275
|
const { root, mission } = await missionAuthority(context.sessionID);
|
|
3156
3276
|
if (args.executor !== undefined && !["self", "worker"].includes(args.executor))
|
|
3157
3277
|
throw new Error("mission-executor-invalid");
|
|
@@ -3360,8 +3480,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3360
3480
|
objective: `${validationRecovery ? "The prior correction completed, but its inherited validation is now stale. Preserve completed implementation, reproduction, original findings and historical checks; refresh only affected checks in this selected author context. If the check recipe needs repair, use repair_review here as its actual author. Do not redo unchanged implementation/discovery or request a routine repeat Review. " : ""}Correct all retained concrete Major/Medium findings in this selected native conversation; preserve every original requirement, prior author and evidence binding. Exact findings are at correction_context.findings in the handoff; retained_findings_ref is optional lineage, not an instruction to reread/print the entire Mission or previous review prompt. Relevant search and focused diagnostics may widen normally; do not rediscover unchanged work. ${REVIEWER_VALIDATION_WORKFLOW} Retain the requested commit/clean boundary. THEN explicitly self-recheck the original requirements, ALL retained findings, correction and relevant impact in this Task. Correct known defects and revalidate affected checks before finishing. First line SELF_RECHECKED. Next line self_recheck: {"candidate":"current-validated","unresolved_findings":[],"residual_major":null}. The HOST binds current-validated to actual current source and fresh successful native checks after this exact prompt; never copy a pre-edit hash. Explain the actual comparison below. List unresolved concrete Major/Medium defects in unresolved_findings (no acceptance). Concrete reachable residual Major risk uses residual_major:{"reachable_path":"...","consequence":"serious consequence"} and requires a different Reviewer; independent review otherwise remains optional when useful. Tags/hashes/Medium/prose gaps alone never mandate it. This is author self-recheck, NOT independent PASS. Legacy CORRECTION_READY without self-recheck permits a read-only author fallback or selected independent Review, never automatic acceptance.`,
|
|
3361
3481
|
read: [...new Set([...(mission.reviewScope?.read ?? []), ...run.units.flatMap(unit => unit.unit.read)])],
|
|
3362
3482
|
write: [...new Set([...(mission.reviewScope?.write ?? []), ...run.units.flatMap(unit => unit.unit.write)])],
|
|
3363
|
-
validation: run.units.flatMap(
|
|
3364
|
-
validation_cwd: Object.assign({}, ...run.units.map(unit => unit
|
|
3483
|
+
validation: run.units.flatMap(unitValidation),
|
|
3484
|
+
validation_cwd: Object.assign({}, ...run.units.map(unit => unitValidationCwd(unit) ?? {})) }], input.directory);
|
|
3365
3485
|
// Inherit fixed declaration budgets and Git authority, never infer fresh capacity.
|
|
3366
3486
|
const goalPath = /^goal_declaration_path: (.+)$/mu.exec(run.units[0].task.prompt)?.[1];
|
|
3367
3487
|
if (!goalPath)
|
|
@@ -3369,7 +3489,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3369
3489
|
const declaration = JSON.parse(await readFile(goalPath, "utf8"));
|
|
3370
3490
|
const retainedPlan = { ...plan, acceptance: run.acceptance, acceptance_proof: run.acceptanceProof,
|
|
3371
3491
|
source_refs: run.sourceRefs, goal_declaration: declaration,
|
|
3372
|
-
units: [{ ...plan.units[0], validation: run.units.flatMap(
|
|
3492
|
+
units: [{ ...plan.units[0], validation: run.units.flatMap(unitValidation) }] };
|
|
3373
3493
|
const correctedPlan = run.gitLifecycle ? { ...retainedPlan, git_lifecycle: {
|
|
3374
3494
|
branch_create: { branch: run.gitLifecycle.branch, start_ref: run.gitLifecycle.startRef },
|
|
3375
3495
|
commit: { message: run.gitLifecycle.commitMessage }, post_commit_validation: run.gitLifecycle.postCommitValidation,
|
|
@@ -3520,9 +3640,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3520
3640
|
}).catch(() => []) : [];
|
|
3521
3641
|
const initialPrompt = initialMissionReviewPrompt(mission, completed);
|
|
3522
3642
|
const phase = initialPrompt ? "verification" : "initial";
|
|
3523
|
-
const observedValidation = risk.length === 0 && !correction ? [] : await Promise.all(run.units.filter(unit => unit.
|
|
3643
|
+
const observedValidation = risk.length === 0 && !correction ? [] : await Promise.all(run.units.filter(unit => unitValidation(unit).length > 0).map(async (unit) => ({
|
|
3524
3644
|
unit_id: unit.unit.id,
|
|
3525
|
-
...observedMissionValidation(unit
|
|
3645
|
+
...observedMissionValidation(unitValidation(unit), unit.childSessionID, unit.childSessionID ? await messages(unit.childSessionID).catch(() => []) : [], unit.reviewerCorrection ? Date.parse(unit.reviewerCorrection.admittedAt ?? run.createdAt) : undefined, unit.reviewerCorrection || unit.directExecution ? input.directory : undefined, unitValidationCwd(unit)),
|
|
3526
3646
|
})));
|
|
3527
3647
|
const task = risk.length === 0 && !correction && !selfRecheck && !independentRequested ? null : { subagent_type: profileAgent(profile, "dog-reviewer"),
|
|
3528
3648
|
...(reviewerID ? { task_id: reviewerID } : {}),
|
|
@@ -47,6 +47,13 @@ export interface ReviewerCorrectionCheck {
|
|
|
47
47
|
/** A command that actually generated/formatted declared outputs retains its stable input digest. */
|
|
48
48
|
readonly generatedInputs?: string;
|
|
49
49
|
}
|
|
50
|
+
/** Live projection of reusable occurrences; never rewrites the saved native check. */
|
|
51
|
+
export interface DirectHandoffValidationCheck extends ReviewerCorrectionCheck {
|
|
52
|
+
readonly matchedOccurrences: readonly {
|
|
53
|
+
readonly member: number;
|
|
54
|
+
readonly occurrence: number;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
50
57
|
/** Exact durable Mission Task claim, not authority supplied by a tool argument or Read. */
|
|
51
58
|
export interface MissionImplementationAdmission {
|
|
52
59
|
readonly runID: string;
|
|
@@ -82,6 +89,8 @@ export interface RuntimeBridge {
|
|
|
82
89
|
/** Only an exact admitted correction Task is an implementation dispatch continuing the original Reviewer child. */
|
|
83
90
|
ownsReviewerCorrectionDispatch?(rootSessionID: string, callID: string, taskID: string): Promise<boolean>;
|
|
84
91
|
ownsReviewerCorrection?(childSessionID: string): Promise<boolean>;
|
|
92
|
+
/** Native check capture only; does not grant Reviewer correction or artifact authority. */
|
|
93
|
+
ownsValidationRegistration?(childSessionID: string): Promise<boolean>;
|
|
85
94
|
/** Existing controller executing a declared unit itself; never a fictitious native Task. */
|
|
86
95
|
directMissionExecution?(sessionID: string): Promise<{
|
|
87
96
|
root: string;
|
|
@@ -99,7 +108,7 @@ export interface RuntimeBridge {
|
|
|
99
108
|
endedAt: string;
|
|
100
109
|
}[];
|
|
101
110
|
} | undefined>;
|
|
102
|
-
missionHandoffChecks?(rootSessionID: string, taskID: string, callID: string): Promise<readonly
|
|
111
|
+
missionHandoffChecks?(rootSessionID: string, taskID: string, callID: string): Promise<readonly DirectHandoffValidationCheck[]>;
|
|
103
112
|
reviewerCorrectionValidationMembers?(childSessionID: string, command: string, directory?: string): Promise<string[] | undefined>;
|
|
104
113
|
missionValidationDirectoryMatches?(rootSessionID: string, taskID: string, childSessionID: string, commands: readonly string[], directory: string): Promise<boolean>;
|
|
105
114
|
recordReviewerCorrectionCheck?(rootSessionID: string, taskID: string, check: ReviewerCorrectionCheck): Promise<void>;
|
|
@@ -156,6 +165,7 @@ export interface RuntimeBridge {
|
|
|
156
165
|
currentTaskIDs: readonly string[];
|
|
157
166
|
}): Promise<void>;
|
|
158
167
|
registerGoalDeclaration(rootSessionID: string, prompt: string, missionRevision?: boolean): Promise<void>;
|
|
168
|
+
registerDirectValidation(rootSessionID: string, actor: string, criterionID: string, command: string, registeredAt: string): Promise<void>;
|
|
159
169
|
relinkRegisteredGoal(rootSessionID: string, request: {
|
|
160
170
|
prompt: string;
|
|
161
171
|
expectedFingerprint: string;
|
|
@@ -163,6 +163,18 @@ ${controls(profile, ["start_mission", "plan_units", "start_direct_unit", "finish
|
|
|
163
163
|
You are Operator, the user-facing strategic authority. Preserve every current requirement, prohibition,
|
|
164
164
|
quality threshold and explicit model/budget choice. Follow AGENTS.md and use the user's language.
|
|
165
165
|
|
|
166
|
+
Own strategy, not just the relay of a stopped author's reply. At a method stall or proposed extra step,
|
|
167
|
+
compare the user's goal, actual result and concrete unmet work. Continue planned useful work without
|
|
168
|
+
intervention; suppress unnecessary expansion, and choose only the recovery needed for a real obstacle.
|
|
169
|
+
Separate a required outcome/check from an AI-selected method. Do not promote your own checker, evidence
|
|
170
|
+
format or procedure into an immutable user requirement. An AI-selected method already registered as
|
|
171
|
+
formal validation still needs lawful recipe correction or replanning; never silently omit it or invent PASS.
|
|
172
|
+
When a change of method is needed, give the existing author the concrete alternative, rationale and
|
|
173
|
+
retained obligations, not merely "resume" or the same failing instruction. Use plan_units with proposed
|
|
174
|
+
units, reason and handoff_context to obtain its concrete Coordinator continuation when available.
|
|
175
|
+
Do not restart a release, cancel live work or take over implementation just to exercise strategic authority.
|
|
176
|
+
Ordinary implementation decisions remain with the author; no per-Task approval or decision-record ritual.
|
|
177
|
+
|
|
166
178
|
${MISSION_DEPENDENCY_WORKFLOW}
|
|
167
179
|
|
|
168
180
|
1. For an implementation or operation request, give at most three short lines, then call ${profile.toolPrefix}start_mission
|
|
@@ -195,7 +207,8 @@ ${MISSION_DEPENDENCY_WORKFLOW}
|
|
|
195
207
|
an ordinary affirmative reply or a request to self-recover is enough. No hidden command or ID is
|
|
196
208
|
needed from the user, and ordinary autonomous recovery does not require a new approval.
|
|
197
209
|
Inspect operator_status to reconcile only native-proven ended work. If this session owns a quiescent
|
|
198
|
-
direct admission that cannot bind, plan_units with executor: "worker" and
|
|
210
|
+
direct admission that cannot bind, plan_units with executor: "worker" and units: "" in V2
|
|
211
|
+
(omit units on the legacy API; never units: []) yields that same
|
|
199
212
|
unit and reservation without acquiring the contested writer. Explain the result and continue the
|
|
200
213
|
returned Task when its write scope is available. A genuinely live or unproven other owner is not
|
|
201
214
|
released: wait or separate work, and do not dispatch repeated conflicting attempts. Do not invent
|
|
@@ -545,14 +558,16 @@ Use the Operator's preferred_executor as a starting recommendation. Start useful
|
|
|
545
558
|
do not investigate merely to confirm the recommendation. Adapt through actual work, honoring explicit user choices.
|
|
546
559
|
Use plan_units with executor: "self" for direct work, or start_direct_unit for an already prepared unit.
|
|
547
560
|
If a direct admission cannot bind because of manifest-overlap, operator_status and plan_units with
|
|
548
|
-
executor: "worker" and
|
|
561
|
+
executor: "worker" and units: "" in V2 (omit units on the legacy API; never units: []) remain
|
|
562
|
+
management operations: yield only this quiescent owner, retain
|
|
549
563
|
its checks and reservation, and wait for a real conflicting writer rather than evicting it. Continue
|
|
550
564
|
the same Mission and actual remaining work; do not create another Mission to clear the conflict.
|
|
551
565
|
The host binds your native check results to the candidate; finish_direct_unit closes that work without
|
|
552
566
|
a Worker handoff. Independent Review and Operator final acceptance still follow. This is not limited to
|
|
553
567
|
micro-edits. Delegate to the configured Worker when that reduces work/cost or provides useful separation.
|
|
554
568
|
There is no proposal/approval/contract-repair round trip in this route.
|
|
555
|
-
To hand unfinished direct work to a Worker, call plan_units with executor: "worker",
|
|
569
|
+
To hand unfinished direct work to a Worker, call plan_units with executor: "worker", units: "" in V2
|
|
570
|
+
(omit units on the legacy API; never units: []), and put
|
|
556
571
|
the remaining action/useful observations in handoff_context. The host transfers this same unit after current
|
|
557
572
|
native commands settle, retaining changes, checks and budget. Do not finish it as successful or replan just
|
|
558
573
|
to change its author. Dispatch the returned Task once; Operator recommendation does not prevent this choice.
|