sortie-dogs 0.13.16 → 0.13.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +10 -0
- package/dist/core/goal-bound.js +20 -7
- package/dist/core/operator-mission.d.ts +7 -0
- package/dist/core/operator-mission.js +10 -6
- package/dist/core/operator-runtime.d.ts +8 -1
- package/dist/core/operator-runtime.js +24 -14
- package/dist/core/run-flight-ledger.js +8 -0
- package/dist/core/validation-budget.js +3 -1
- package/dist/plugin/gate.d.ts +4 -0
- package/dist/plugin/gate.js +34 -0
- package/dist/plugin/index.js +103 -7
- package/dist/plugin/mission-review.d.ts +1 -0
- package/dist/plugin/mission-review.js +15 -10
- package/dist/plugin/operator-status-output.d.ts +6 -0
- package/dist/plugin/operator-status-output.js +19 -0
- package/dist/plugin/profiled.js +240 -139
- package/dist/plugin/runtime-bridge.d.ts +10 -1
- package/dist/plugin/v2.js +18 -7
- package/dist/runtime-mission-assets.d.ts +10 -4
- package/dist/runtime-mission-assets.js +58 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**Current release: [v0.13.
|
|
23
|
-
· [Release notes](docs/release-v0.13.
|
|
22
|
+
**Current release: [v0.13.18](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.18)**
|
|
23
|
+
· [Release notes](docs/release-v0.13.18.md)
|
|
24
24
|
|
|
25
25
|
[Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
26
26
|
[Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
|
|
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
57
57
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
npm install --save-dev sortie-dogs@0.13.
|
|
60
|
+
npm install --save-dev sortie-dogs@0.13.18
|
|
61
61
|
npx --no-install sortie-dogs codex init .
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
86
86
|
Use **OpenCode V2**. In the target project:
|
|
87
87
|
|
|
88
88
|
```sh
|
|
89
|
-
npm install --save-dev sortie-dogs@0.13.
|
|
89
|
+
npm install --save-dev sortie-dogs@0.13.18
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -188,7 +188,7 @@ known expense. Official local evaluation and leaderboard acceptance are separate
|
|
|
188
188
|
**8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
|
|
189
189
|
official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
|
|
190
190
|
|
|
191
|
-
All scores belong to their fixed candidates, **not v0.13.
|
|
191
|
+
All scores belong to their fixed candidates, **not v0.13.18**. SWE-bench remains an optional
|
|
192
192
|
measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
|
|
193
193
|
|
|
194
194
|
## Configuration
|
|
@@ -211,7 +211,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
211
211
|
|
|
212
212
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
213
213
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
214
|
-
The current Mission asset marker is `0.13.
|
|
214
|
+
The current Mission asset marker is `0.13.18-direct-settlement-v1`; the unchanged Codex skill marker is
|
|
215
215
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
216
216
|
|
|
217
217
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -227,7 +227,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
227
227
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
228
228
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
229
229
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
230
|
-
- **Releases:** [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
230
|
+
- **Releases:** [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
231
231
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
232
232
|
|
|
233
233
|
## Community
|
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.13.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.13.18-direct-settlement-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
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.13.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.13.18-direct-settlement-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -111,6 +111,15 @@ export interface GoalTerminalReceipt {
|
|
|
111
111
|
interface GoalEventBase {
|
|
112
112
|
readonly at: string;
|
|
113
113
|
}
|
|
114
|
+
/** Host verification of an existing direct finish receipt, not a new execution. */
|
|
115
|
+
export interface GoalDirectFinishRecovery {
|
|
116
|
+
readonly call_id: string;
|
|
117
|
+
readonly actor_session_id: string;
|
|
118
|
+
readonly finish_call_id: string;
|
|
119
|
+
readonly started_at: string;
|
|
120
|
+
readonly finished_at: string;
|
|
121
|
+
readonly validation_call_ids: readonly string[];
|
|
122
|
+
}
|
|
114
123
|
export interface GoalValidationRetryAuthorization {
|
|
115
124
|
readonly authority: "operator-repair-validation-retry";
|
|
116
125
|
readonly operator_run_id: string;
|
|
@@ -218,6 +227,7 @@ export type GoalFlightEvent = (GoalEventBase & {
|
|
|
218
227
|
readonly unit_id: string;
|
|
219
228
|
readonly previous_receipt_id: string;
|
|
220
229
|
readonly evidence: readonly GoalEvidence[];
|
|
230
|
+
readonly direct_finish?: GoalDirectFinishRecovery;
|
|
221
231
|
}) | (GoalEventBase & {
|
|
222
232
|
readonly kind: "validation.admission";
|
|
223
233
|
readonly goal_id: string;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -133,7 +133,7 @@ export function reduceGoalFlight(records) {
|
|
|
133
133
|
let replanRevision = null;
|
|
134
134
|
let legacyResetRevision = false;
|
|
135
135
|
let previous = null;
|
|
136
|
-
const
|
|
136
|
+
const recoverableSettlements = new Map();
|
|
137
137
|
const unitCosts = new Map();
|
|
138
138
|
const validationAdmissions = new Map();
|
|
139
139
|
const validationSettlements = new Map();
|
|
@@ -160,7 +160,7 @@ export function reduceGoalFlight(records) {
|
|
|
160
160
|
session_ids: [event.origin_session_id] };
|
|
161
161
|
replanRevision = null;
|
|
162
162
|
legacyResetRevision = false;
|
|
163
|
-
|
|
163
|
+
recoverableSettlements.clear();
|
|
164
164
|
validationAdmissions.clear();
|
|
165
165
|
validationSettlements.clear();
|
|
166
166
|
reopenedValidationEvidence.clear();
|
|
@@ -255,8 +255,8 @@ export function reduceGoalFlight(records) {
|
|
|
255
255
|
unit_ids: addUnique(state.unit_ids, event.unit_id),
|
|
256
256
|
evidence_refs: event.evidence.reduce((refs, evidence) => addUnique(refs, evidence.evidence_id), state.evidence_refs),
|
|
257
257
|
satisfied_criteria: event.evidence.reduce((ids, evidence) => evidence.measurement.criterion_ids.reduce(addUnique, ids), state.satisfied_criteria) };
|
|
258
|
-
if (event.
|
|
259
|
-
|
|
258
|
+
if (event.evidence.length === 0 && event.disposition !== "succeeded") {
|
|
259
|
+
recoverableSettlements.set(event.receipt_id, { unitID: event.unit_id, revision: state.revision, epoch: state.scope_epoch, settlement: event });
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
else if (event.kind === "unit.usage-reconciled") {
|
|
@@ -271,11 +271,24 @@ export function reduceGoalFlight(records) {
|
|
|
271
271
|
: costs.reduce((sum, item) => sum + item.cost, 0) };
|
|
272
272
|
}
|
|
273
273
|
else if (event.kind === "unit.evidence-reconciled") {
|
|
274
|
-
const old =
|
|
274
|
+
const old = recoverableSettlements.get(event.previous_receipt_id);
|
|
275
|
+
const direct = event.direct_finish, settled = old?.settlement;
|
|
276
|
+
const directFinish = direct !== undefined && text(direct.call_id) && direct.call_id.startsWith("direct-") &&
|
|
277
|
+
text(direct.actor_session_id) && text(direct.finish_call_id) && instant(direct.started_at) && instant(direct.finished_at) &&
|
|
278
|
+
Date.parse(direct.finished_at) >= Date.parse(direct.started_at) && Array.isArray(direct.validation_call_ids) && direct.validation_call_ids.length > 0 &&
|
|
279
|
+
direct.validation_call_ids.every(text) && settled?.native_session_id === direct.actor_session_id &&
|
|
280
|
+
settled.native_started_at !== undefined && Math.abs(Date.parse(settled.native_started_at) - Date.parse(direct.started_at)) <= 1000 &&
|
|
281
|
+
Math.abs(Date.parse(settled.at) - Date.parse(direct.finished_at)) <= 1000 &&
|
|
282
|
+
settled.reservation_id === goalFingerprint({ goal_id: event.goal_id, unit_id: event.unit_id, call_id: direct.call_id }) &&
|
|
283
|
+
settled.receipt_id === goalFingerprint({ call_id: direct.call_id,
|
|
284
|
+
output: "Host-observed direct unit finished; controller session remains active." }) &&
|
|
285
|
+
(settled.disposition === "cancelled" && settled.result_class === "interrupted" ||
|
|
286
|
+
settled.disposition === "failed" && ["process-defect", "acceptance"].includes(settled.result_class ?? ""));
|
|
287
|
+
const processDefect = direct === undefined && settled?.disposition !== "cancelled" && settled?.result_class === "process-defect";
|
|
275
288
|
requireState(state.phase === "active" && state.receipt === null && old?.unitID === event.unit_id &&
|
|
276
|
-
old.revision === state.revision && old.epoch === state.scope_epoch && state.outstanding_reservations.length === 0, "transition", "Evidence reconciliation requires a current settled process defect.");
|
|
289
|
+
old.revision === state.revision && old.epoch === state.scope_epoch && state.outstanding_reservations.length === 0 && (processDefect || directFinish), "transition", "Evidence reconciliation requires a current settled process defect or verified direct finish defect.");
|
|
277
290
|
requireState(event.evidence.length > 0 && event.evidence.every(entry => validGoalEvidence(entry, state) && entry.execution.units.includes(event.unit_id)), "evidence", "Reconciled evidence must retain the current goal and unit identity.");
|
|
278
|
-
|
|
291
|
+
recoverableSettlements.delete(event.previous_receipt_id);
|
|
279
292
|
state = { ...state, evidence_refs: event.evidence.reduce((refs, evidence) => addUnique(refs, evidence.evidence_id), state.evidence_refs),
|
|
280
293
|
satisfied_criteria: event.evidence.reduce((ids, evidence) => evidence.measurement.criterion_ids.reduce(addUnique, ids), state.satisfied_criteria) };
|
|
281
294
|
}
|
|
@@ -76,6 +76,13 @@ export interface MissionAttempt {
|
|
|
76
76
|
code: string;
|
|
77
77
|
};
|
|
78
78
|
resultClass?: string;
|
|
79
|
+
/** Retained erroneous finish projection; the original goal receipt remains append-only. */
|
|
80
|
+
registrationRecovery?: {
|
|
81
|
+
at: string;
|
|
82
|
+
previousStatus: MissionAttempt["status"];
|
|
83
|
+
previousResultClass?: string;
|
|
84
|
+
previousFailure?: MissionAttempt["failure"];
|
|
85
|
+
};
|
|
79
86
|
selectedModel?: string;
|
|
80
87
|
}
|
|
81
88
|
export interface MissionTerminalRescue {
|
|
@@ -224,7 +224,11 @@ export class OperatorMissionRuntime {
|
|
|
224
224
|
continuity: { notes: state.continuity?.notes, sources, progress: state.progress,
|
|
225
225
|
attempts: (state.attempts ?? []).map(({ runID, unitID, status, failure, resultClass, childSessionID }) => ({ run_id: runID, unit_id: unitID, status, failure, result_class: resultClass, session_id: childSessionID })),
|
|
226
226
|
execution: state.execution,
|
|
227
|
-
|
|
227
|
+
source_authors: state.sourceAuthors ?? [],
|
|
228
|
+
corrections: (state.corrections ?? []).map(({ initialPrompt: _prompt, inlineReview: _inline, ...correction }) => correction),
|
|
229
|
+
retained_review: state.review ? { run_id: state.review.runID, verdict: state.review.verdict,
|
|
230
|
+
mode: state.review.mode, author: state.review.child, result: state.review.result, self_recheck: state.review.selfRecheck } : null,
|
|
231
|
+
guidance: "Current original_requests/requirements govern; older plans remain historical. Continue from known decisions, environment, completed work, retained findings and failures. A new plan or author does not resolve findings or turn self-recheck into independent approval. Read snapshot JSON entries by message/call IDs only for a relevant gap. Recheck mutable facts when needed or conflicting; do not repeat unchanged setup/investigation/checks merely because ownership changed. Successful evidence remains with its actual author/call and input bindings; historical results are not new authority." } };
|
|
228
232
|
}
|
|
229
233
|
async load(file) {
|
|
230
234
|
try {
|
|
@@ -804,14 +808,14 @@ export function missionPacket(mission, run) {
|
|
|
804
808
|
? "Operator: use acceptance_summary to compare the submitted candidate with the verbatim original requests and actual evidence, inspect concrete gaps only, then complete_mission if satisfied. Do not routinely search archives or reread all source. Historical PASS is not current PASS. Report remaining evidence gaps; they are not a review PASS."
|
|
805
809
|
: run?.phase === "awaiting-acceptance" && mission.corrections?.some(item => item.runID === run.runID && item.status === "ready") && !reviewAccepted
|
|
806
810
|
? mission.corrections.find(item => item.runID === run.runID)?.selfRecheck?.unresolvedFindings.length
|
|
807
|
-
? "Known Major/Medium findings remain after self-recheck.
|
|
811
|
+
? "Known Major/Medium findings remain after self-recheck. Correct them with repair_review (reuse the author by default, or select an available Reviewer), or plan_units for Worker/direct execution. Retain findings, original requirements and cumulative spend; unresolved Medium is not acceptance."
|
|
808
812
|
: mission.corrections.find(item => item.runID === run.runID)?.selfRecheck?.residualMajor
|
|
809
813
|
? "A concrete reachable Major risk remains after native author self-recheck. Call review_mission for a DIFFERENT Reviewer of the correction, retained findings and relevant impact; dispatch the exact returned Task if not already active. No fresh Worker or unchanged validation is required."
|
|
810
|
-
: "Correction ready;
|
|
814
|
+
: "Correction ready; reuse the native author for explicit self-recheck or select an independent Reviewer with review_mission when useful. Remaining authorized implementation may proceed with plan_units without acceptance. Known Major/Medium defects still require correction. Do not restart unchanged checks or investigation; CORRECTION_READY alone is not acceptance."
|
|
811
815
|
: mission.kind === "operation" && operationStatus === "running"
|
|
812
816
|
? "The declared operation is already running. Inspect its native shell/progress; do not start another Worker or run. Wait for a terminal result, or report the existing run as blocked if its completion cannot be observed."
|
|
813
817
|
: run?.phase === "awaiting-decision" && mission.corrections?.some(item => item.runID === run.runID && item.status === "failed")
|
|
814
|
-
? "Correction validation or native execution failed.
|
|
818
|
+
? "Correction validation or native execution failed. Reuse its context with repair_review, select another available Reviewer, or plan_units for Worker/direct correction. Retain findings, affected checks, requested delivery and cumulative spend. Failure is never acceptance."
|
|
815
819
|
: run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
|
|
816
820
|
? "Coordinator: inspect units[].dispatch_denial before changing the plan. Correct only its diagnosed cause; do not repeat an unchanged refused Task or replan for a host-state mismatch. Report an unresolved runtime mismatch with the loaded runtime identity; preserve requirements and cumulative spend."
|
|
817
821
|
: run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && !unit.normalRemediationUsed)
|
|
@@ -822,8 +826,8 @@ export function missionPacket(mission, run) {
|
|
|
822
826
|
: run?.phase === "awaiting-acceptance" && !operationComplete
|
|
823
827
|
? `Requested operation is ${operationStatus}. Continue the actual operation or report its blocker; auxiliary checks and review disposition cannot complete it.`
|
|
824
828
|
: run?.phase === "awaiting-acceptance" ? (reviewAccepted
|
|
825
|
-
? "Coordinator: review permits submission. Retain
|
|
826
|
-
: "Coordinator: correct known Major/Medium findings, then obtain
|
|
829
|
+
? "Coordinator: review permits submission, not completion of remaining implementation. Continue authorized remaining work with plan_units, or submit when the original requirements are met. Retain review notes; do not repeat passed validation or review for evidence formatting. Operator final acceptance remains separate."
|
|
830
|
+
: "Coordinator: correct known Major/Medium findings, then obtain native author self-recheck or useful independent review (required for concrete residual Major risk). Remaining authorized implementation can proceed without submission/acceptance. Operator compares all requirements with source/evidence before complete_mission.")
|
|
827
831
|
: "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
|
|
828
832
|
}
|
|
829
833
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
@@ -131,6 +131,11 @@ interface UnitState {
|
|
|
131
131
|
actor: string;
|
|
132
132
|
startedAt: string;
|
|
133
133
|
finishedAt?: string;
|
|
134
|
+
registrationRecovery?: {
|
|
135
|
+
at: string;
|
|
136
|
+
previousStatus: "failed" | "cancelled";
|
|
137
|
+
previousResultClass: string | null;
|
|
138
|
+
};
|
|
134
139
|
checks: import("../plugin/runtime-bridge.js").ReviewerCorrectionCheck[];
|
|
135
140
|
validationCwd?: Readonly<Record<string, string>>;
|
|
136
141
|
validationRegisteredAt?: Readonly<Record<string, string>>;
|
|
@@ -280,6 +285,8 @@ export declare class OperatorRuntime {
|
|
|
280
285
|
private file;
|
|
281
286
|
/** Exact persisted source for read-only status projections. */
|
|
282
287
|
statePath(root: string): string;
|
|
288
|
+
/** Stable detail source for a settled run replaced by Mission replanning. */
|
|
289
|
+
replanArchivePath(state: OperatorState): string;
|
|
283
290
|
/** Read-only lineage projection. Never imports archived evidence into current acceptance. */
|
|
284
291
|
acceptanceHistory(state: OperatorState, missionRunIDs: readonly string[]): Promise<{
|
|
285
292
|
status: "available" | "unavailable";
|
|
@@ -428,7 +435,7 @@ export declare class OperatorRuntime {
|
|
|
428
435
|
private repairValidationRetryBinding;
|
|
429
436
|
canRetryRepairValidation(state: OperatorState): boolean;
|
|
430
437
|
resumeRepairValidation(root: string, runID: string, authorize: (source: OperatorRepairValidationRetrySource) => Promise<void>): Promise<OperatorState>;
|
|
431
|
-
resume(root: string, runID: string, recovered: ReadonlyMap<string, readonly GoalEvidence[]>, unstarted?: ReadonlySet<string>): Promise<OperatorState>;
|
|
438
|
+
resume(root: string, runID: string, recovered: ReadonlyMap<string, readonly GoalEvidence[]>, unstarted?: ReadonlySet<string>, recoveredDirect?: ReadonlySet<string>): Promise<OperatorState>;
|
|
432
439
|
requireAcceptanceRemediation(root: string, runID: string): Promise<OperatorState>;
|
|
433
440
|
draftStatus(root: string): Promise<unknown | undefined>;
|
|
434
441
|
completionGoalFingerprint(state: OperatorState): Promise<string>;
|
|
@@ -276,6 +276,10 @@ export class OperatorRuntime {
|
|
|
276
276
|
}
|
|
277
277
|
/** Exact persisted source for read-only status projections. */
|
|
278
278
|
statePath(root) { return this.file(root); }
|
|
279
|
+
/** Stable detail source for a settled run replaced by Mission replanning. */
|
|
280
|
+
replanArchivePath(state) {
|
|
281
|
+
return `${this.file(state.rootSessionID)}.${state.runID}.${state.sequence}.archive`;
|
|
282
|
+
}
|
|
279
283
|
/** Read-only lineage projection. Never imports archived evidence into current acceptance. */
|
|
280
284
|
async acceptanceHistory(state, missionRunIDs) {
|
|
281
285
|
const runs = [];
|
|
@@ -878,14 +882,12 @@ export class OperatorRuntime {
|
|
|
878
882
|
if (!previous || previous.runID !== mission.replaceRunID || ["cancelled", "completed"].includes(previous.phase)) {
|
|
879
883
|
throw new Error("mission-replan-run-mismatch");
|
|
880
884
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (previous.units.some(unit => unit.status === "running") || (previous.gitLifecycle !== null && !mission.reviewerCorrection))
|
|
885
|
+
// Completed correction/Git metadata is history, not a live writer lease.
|
|
886
|
+
// The plugin reconciles actual native executions before replacing this run.
|
|
887
|
+
if (previous.units.some(unit => unit.status === "running"))
|
|
885
888
|
throw new Error("mission-replan-worker-still-active");
|
|
886
|
-
const failedContinuation = previous.phase === "awaiting-decision" && previous.units.every(unit => unit.status === "failed" && unit.reviewerCorrection
|
|
889
|
+
const failedContinuation = previous.phase === "awaiting-decision" && previous.units.every(unit => unit.status === "failed" && unit.reviewerCorrection);
|
|
887
890
|
if (mission.reviewerCorrection && ((!failedContinuation && (previous.phase !== "awaiting-acceptance" || previous.units.some(unit => unit.status !== "succeeded"))) ||
|
|
888
|
-
previous.units.some(unit => unit.reviewerCorrection && unit.reviewerCorrection.author !== mission.reviewerCorrection.author) ||
|
|
889
891
|
plan.units.length !== 1 || plan.acceptance.length !== previous.acceptance.length ||
|
|
890
892
|
previous.acceptance.some((text, i) => text !== plan.acceptance[i]) ||
|
|
891
893
|
JSON.stringify(plan.acceptance_proof) !== JSON.stringify(previous.acceptanceProof) ||
|
|
@@ -1166,10 +1168,11 @@ export class OperatorRuntime {
|
|
|
1166
1168
|
await handle.close();
|
|
1167
1169
|
}
|
|
1168
1170
|
}
|
|
1169
|
-
if ((superseding || replanning) && previous) {
|
|
1171
|
+
if ((superseding || replanning || previous?.phase === "completed") && previous) {
|
|
1170
1172
|
// A crash before the state switch may leave a checkpoint while the old run later advances.
|
|
1171
1173
|
// Sequence-qualified replan snapshots keep that history without blocking a subsequent correction.
|
|
1172
|
-
const archive =
|
|
1174
|
+
const archive = replanning || previous.phase === "completed" ? this.replanArchivePath(previous)
|
|
1175
|
+
: `${this.file(root)}.${previous.runID}.archive`;
|
|
1173
1176
|
try {
|
|
1174
1177
|
const handle = await open(archive, "wx", 0o600);
|
|
1175
1178
|
created.push(archive);
|
|
@@ -1323,8 +1326,6 @@ export class OperatorRuntime {
|
|
|
1323
1326
|
return args.task_id === expected.task_id && args.subagent_type === expected.subagent_type &&
|
|
1324
1327
|
args.description === expected.description && args.prompt === expected.prompt;
|
|
1325
1328
|
}
|
|
1326
|
-
if (unit.reviewerCorrection && (args.model !== undefined || args.variant !== undefined))
|
|
1327
|
-
return false;
|
|
1328
1329
|
if (unit.reviewerCorrection ? args.task_id !== unit.reviewerCorrection.author : args.task_id !== undefined && args.task_id !== "")
|
|
1329
1330
|
return false;
|
|
1330
1331
|
if (args.subagent_type !== unit.task.subagent_type || args.description !== unit.task.description)
|
|
@@ -2090,16 +2091,17 @@ export class OperatorRuntime {
|
|
|
2090
2091
|
return state;
|
|
2091
2092
|
});
|
|
2092
2093
|
}
|
|
2093
|
-
resume(root, runID, recovered, unstarted = new Set()) {
|
|
2094
|
+
resume(root, runID, recovered, unstarted = new Set(), recoveredDirect = new Set()) {
|
|
2094
2095
|
return this.serial(root, async () => {
|
|
2095
2096
|
const state = await this.required(root);
|
|
2096
2097
|
if (state.contractRepair !== null)
|
|
2097
2098
|
throw new Error("operator-resume-contract-repair-required");
|
|
2098
|
-
if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(unit => unit.status === "running" ||
|
|
2099
|
+
if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(unit => unit.status === "running" ||
|
|
2100
|
+
unit.status === "cancelled" && !recoveredDirect.has(unit.unit.id))) {
|
|
2099
2101
|
throw new Error("operator-resume-not-ready");
|
|
2100
2102
|
}
|
|
2101
2103
|
for (const unit of state.units) {
|
|
2102
|
-
if (unit.status !== "failed")
|
|
2104
|
+
if (unit.status !== "failed" && unit.status !== "cancelled")
|
|
2103
2105
|
continue;
|
|
2104
2106
|
if (unstarted.has(unit.unit.id)) {
|
|
2105
2107
|
if (unit.resultClass !== "process-defect" || unit.childSessionID !== null || state.decision !== "dispatch-admission-rejected")
|
|
@@ -2110,10 +2112,18 @@ export class OperatorRuntime {
|
|
|
2110
2112
|
continue;
|
|
2111
2113
|
}
|
|
2112
2114
|
const evidence = recovered.get(unit.unit.id);
|
|
2115
|
+
const direct = recoveredDirect.has(unit.unit.id) && unit.directExecution?.actor === unit.childSessionID &&
|
|
2116
|
+
!!unit.directExecution.finishedAt && !!unit.callID && !!evidence?.length;
|
|
2117
|
+
await this.verifyControls(unit);
|
|
2113
2118
|
const ids = new Set(evidence?.flatMap(item => item.measurement.criterion_ids) ?? []);
|
|
2114
|
-
if (unit.resultClass !== "process-defect" || !evidence?.length || !unit.unit.acceptance_indices.every(index => state.acceptanceProof[index].some(id => ids.has(id)))) {
|
|
2119
|
+
if (unit.resultClass !== "process-defect" && !direct || !evidence?.length || !unit.unit.acceptance_indices.every(index => state.acceptanceProof[index].some(id => ids.has(id)))) {
|
|
2115
2120
|
throw new Error("operator-resume-proof-incomplete");
|
|
2116
2121
|
}
|
|
2122
|
+
if (unit.status === "cancelled" && !direct)
|
|
2123
|
+
throw new Error("operator-resume-proof-incomplete");
|
|
2124
|
+
if (direct)
|
|
2125
|
+
unit.directExecution.registrationRecovery = { at: new Date().toISOString(),
|
|
2126
|
+
previousStatus: unit.status, previousResultClass: unit.resultClass };
|
|
2117
2127
|
unit.status = "succeeded";
|
|
2118
2128
|
unit.resultClass = "acceptance";
|
|
2119
2129
|
unit.evidence = evidence;
|
|
@@ -925,6 +925,14 @@ export class RunFlightLedger {
|
|
|
925
925
|
stored.reservation_id === event.reservation_id && stored.native_session_id === event.native_session_id &&
|
|
926
926
|
stored.cost_usd === event.cost_usd))
|
|
927
927
|
return reduceGoalFlight(records);
|
|
928
|
+
if (event.kind === "unit.evidence-reconciled" && event.direct_finish !== undefined) {
|
|
929
|
+
const existing = records.find(record => record.event.kind === "unit.evidence-reconciled" &&
|
|
930
|
+
record.event.previous_receipt_id === event.previous_receipt_id)?.event;
|
|
931
|
+
if (existing?.kind === "unit.evidence-reconciled" && existing.goal_id === event.goal_id && existing.unit_id === event.unit_id &&
|
|
932
|
+
goalFingerprint(existing.evidence) === goalFingerprint(event.evidence) &&
|
|
933
|
+
goalFingerprint(existing.direct_finish) === goalFingerprint(event.direct_finish))
|
|
934
|
+
return reduceGoalFlight(records);
|
|
935
|
+
}
|
|
928
936
|
if (event.kind === "goal.accepted") {
|
|
929
937
|
const prior = records.find(({ event: stored }) => stored.kind === "goal.accepted" &&
|
|
930
938
|
stored.origin_user_message_id === event.origin_user_message_id);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { CONTRACT_TEXT_LIMITS } from "./contract-limits.js";
|
|
2
3
|
export const VALIDATION_PROFILES = ["fast", "balanced", "assurance"];
|
|
3
4
|
/** A real failed process can be rerun after setup repair without changing source or command identity. */
|
|
4
5
|
export function retryableValidationFailure(outcome, exitCode) {
|
|
@@ -38,6 +39,7 @@ export function validationEvidenceState(events) {
|
|
|
38
39
|
return { reusable: [...reusable], blocked: [...blocked] };
|
|
39
40
|
}
|
|
40
41
|
const id = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096;
|
|
42
|
+
const commandText = (value) => typeof value === "string" && value.length > 0 && value.length <= CONTRACT_TEXT_LIMITS.command;
|
|
41
43
|
const scopes = new Set(["static", "targeted", "related", "canonical", "full-suite", "full"]);
|
|
42
44
|
const normalizedScope = (scope) => scope === "full" ? "canonical" : scope;
|
|
43
45
|
export function validationOwner(scope) {
|
|
@@ -94,7 +96,7 @@ export function decideValidationBudget(request, state) {
|
|
|
94
96
|
const environment = value.environment;
|
|
95
97
|
const marginal = value.marginal_value;
|
|
96
98
|
const valid = id(value.run_id) && id(value.operation_id) && id(value.source_snapshot) && id(value.candidate) &&
|
|
97
|
-
value.command.length > 0 && value.command.every(
|
|
99
|
+
value.command.length > 0 && value.command.every(commandText) && scopes.has(value.scope) &&
|
|
98
100
|
environment !== null && typeof environment === "object" && id(environment.platform) && id(environment.arch) &&
|
|
99
101
|
id(environment.runtime) && (environment.toolchain === undefined || id(environment.toolchain)) &&
|
|
100
102
|
(value.owner === "worker" || value.owner === "coordinator") &&
|
package/dist/plugin/gate.d.ts
CHANGED
|
@@ -70,6 +70,10 @@ export declare function normalizeCommand(command: string): string;
|
|
|
70
70
|
export declare function canonicalDeclaredValidationSequence(command: string, declared: ReadonlySet<string>): string | undefined;
|
|
71
71
|
/** Preserve ordered members (including repeats) without splitting quoted text or substring matching. */
|
|
72
72
|
export declare function canonicalDeclaredValidationMembers(command: string, declared: ReadonlySet<string> | readonly string[], occurrence?: number): string[] | undefined;
|
|
73
|
+
/** Observation only, never shell authority. A successful native && chain can prove
|
|
74
|
+
* its exact declared tail after diagnostics/empty-directory cleanup. The caller
|
|
75
|
+
* must retain the real exit and the snapshot captured BEFORE the whole command. */
|
|
76
|
+
export declare function canonicalObservedValidationMembers(command: string, declared: ReadonlySet<string> | readonly string[], occurrence?: number): string[] | undefined;
|
|
73
77
|
/** Unbound sessions may invoke only tools whose complete input is known to be read-only. */
|
|
74
78
|
export declare function isKnownReadOnlyTool(tool: string, args: unknown, additionalReadOnlyTools?: ReadonlySet<string>): boolean;
|
|
75
79
|
/** Canonical scopes let sibling worker bindings reject equal or ancestor ownership. */
|
package/dist/plugin/gate.js
CHANGED
|
@@ -997,6 +997,40 @@ export function canonicalDeclaredValidationMembers(command, declared, occurrence
|
|
|
997
997
|
};
|
|
998
998
|
return sequence(0);
|
|
999
999
|
}
|
|
1000
|
+
/** Observation only, never shell authority. A successful native && chain can prove
|
|
1001
|
+
* its exact declared tail after diagnostics/empty-directory cleanup. The caller
|
|
1002
|
+
* must retain the real exit and the snapshot captured BEFORE the whole command. */
|
|
1003
|
+
export function canonicalObservedValidationMembers(command, declared, occurrence = 0) {
|
|
1004
|
+
const exact = canonicalDeclaredValidationMembers(command, declared, occurrence);
|
|
1005
|
+
if (exact)
|
|
1006
|
+
return exact;
|
|
1007
|
+
const syntax = scanShellSyntax(command, "posix");
|
|
1008
|
+
// Do not infer a tail through comments, expansions, redirection, subshells or
|
|
1009
|
+
// shell-state changes. Unsupported observations leave existing authority alone.
|
|
1010
|
+
if (syntax.unsafeExpansion || syntax.activeBrace || /[\\#$()<>`]/u.test(syntax.masked))
|
|
1011
|
+
return undefined;
|
|
1012
|
+
const segments = shellSegments(command, "posix").map(segment => segment.trim());
|
|
1013
|
+
if (segments.length < 2 || segments.some(segment => !segment) ||
|
|
1014
|
+
normalizeCommand(command) !== segments.map(normalizeCommand).join(" && "))
|
|
1015
|
+
return undefined;
|
|
1016
|
+
const identities = [...new Set(declared)];
|
|
1017
|
+
for (let start = 1; start < segments.length; start++) {
|
|
1018
|
+
const prefix = segments[start - 1];
|
|
1019
|
+
// Never discard an ordered declared occurrence to find a later matching tail.
|
|
1020
|
+
if (declaredCommandMatch(prefix, identities) !== undefined)
|
|
1021
|
+
return undefined;
|
|
1022
|
+
if (prefix.includes("`"))
|
|
1023
|
+
return undefined;
|
|
1024
|
+
const extracted = shellPaths(prefix, false);
|
|
1025
|
+
const cleanup = /^rmdir(?:\s|$)/u.test(prefix);
|
|
1026
|
+
if (extracted.ambiguous || extracted.remoteMutation || extracted.gitMutation || extracted.applies && !cleanup)
|
|
1027
|
+
return undefined;
|
|
1028
|
+
const members = canonicalDeclaredValidationMembers(segments.slice(start).join(" && "), declared, occurrence);
|
|
1029
|
+
if (members)
|
|
1030
|
+
return members;
|
|
1031
|
+
}
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1000
1034
|
/** Unbound sessions may invoke only tools whose complete input is known to be read-only. */
|
|
1001
1035
|
export function isKnownReadOnlyTool(tool, args, additionalReadOnlyTools = new Set()) {
|
|
1002
1036
|
const name = tool.toLowerCase();
|