sortie-dogs 0.13.15 → 0.13.17
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 +13 -0
- package/dist/core/goal-bound.js +13 -3
- package/dist/core/operator-mission.d.ts +2 -0
- package/dist/core/operator-mission.js +12 -7
- package/dist/core/operator-runtime.d.ts +2 -0
- package/dist/core/operator-runtime.js +22 -14
- package/dist/core/validate-worktree-parallel.js +3 -4
- package/dist/core/worktree-scope.js +3 -2
- package/dist/plugin/fast-lane.js +5 -2
- package/dist/plugin/gate.d.ts +4 -0
- package/dist/plugin/gate.js +34 -0
- package/dist/plugin/index.d.ts +10 -0
- package/dist/plugin/index.js +5 -8
- package/dist/plugin/mission-review.d.ts +1 -0
- package/dist/plugin/mission-review.js +115 -61
- package/dist/plugin/native-background.d.ts +6 -1
- package/dist/plugin/native-background.js +60 -2
- package/dist/plugin/operator-status-output.d.ts +6 -0
- package/dist/plugin/operator-status-output.js +19 -0
- package/dist/plugin/profiled.js +275 -84
- package/dist/plugin/protected-snapshot.d.ts +23 -1
- package/dist/plugin/protected-snapshot.js +209 -5
- package/dist/plugin/v2.js +108 -20
- package/dist/runtime-assets.d.ts +2 -2
- package/dist/runtime-assets.js +2 -2
- package/dist/runtime-mission-assets.d.ts +13 -4
- package/dist/runtime-mission-assets.js +75 -17
- 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.17](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.17)**
|
|
23
|
+
· [Release notes](docs/release-v0.13.17.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.17
|
|
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.17
|
|
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.17**. 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.17-handoff-continuity-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.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.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.17-handoff-continuity-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.17-handoff-continuity-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -51,6 +51,19 @@ export interface GoalEvidence {
|
|
|
51
51
|
readonly scratch_paths: readonly string[];
|
|
52
52
|
readonly protected_paths: readonly string[];
|
|
53
53
|
readonly environment: Readonly<Record<string, string | null>>;
|
|
54
|
+
readonly incidental_control_paths?: readonly string[];
|
|
55
|
+
readonly toolchain?: {
|
|
56
|
+
readonly directory: string;
|
|
57
|
+
readonly commands: readonly string[];
|
|
58
|
+
readonly executables: readonly {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly path: string | null;
|
|
61
|
+
readonly realpath: string | null;
|
|
62
|
+
readonly sha256: string | null;
|
|
63
|
+
readonly mode?: number;
|
|
64
|
+
readonly runtime?: string;
|
|
65
|
+
}[];
|
|
66
|
+
};
|
|
54
67
|
};
|
|
55
68
|
};
|
|
56
69
|
readonly execution: {
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -72,7 +72,17 @@ export function validGoalEvidence(value, state) {
|
|
|
72
72
|
Array.isArray(protectedBinding.freshness.scratch_paths) && protectedBinding.freshness.scratch_paths.every(text) &&
|
|
73
73
|
Array.isArray(protectedBinding.freshness.protected_paths) && protectedBinding.freshness.protected_paths.every(text) &&
|
|
74
74
|
protectedBinding.freshness.environment !== null && typeof protectedBinding.freshness.environment === "object" &&
|
|
75
|
-
!Array.isArray(protectedBinding.freshness.environment) && Object.values(protectedBinding.freshness.environment).every(value => value === null || typeof value === "string")
|
|
75
|
+
!Array.isArray(protectedBinding.freshness.environment) && Object.values(protectedBinding.freshness.environment).every(value => value === null || typeof value === "string") &&
|
|
76
|
+
(protectedBinding.freshness.incidental_control_paths === undefined || Array.isArray(protectedBinding.freshness.incidental_control_paths) &&
|
|
77
|
+
protectedBinding.freshness.incidental_control_paths.every(text)) &&
|
|
78
|
+
(protectedBinding.freshness.toolchain === undefined || protectedBinding.freshness.toolchain !== null &&
|
|
79
|
+
typeof protectedBinding.freshness.toolchain === "object" && text(protectedBinding.freshness.toolchain.directory) &&
|
|
80
|
+
Array.isArray(protectedBinding.freshness.toolchain.commands) && protectedBinding.freshness.toolchain.commands.every(commandText) &&
|
|
81
|
+
Array.isArray(protectedBinding.freshness.toolchain.executables) && protectedBinding.freshness.toolchain.executables.every(executable => executable !== null && typeof executable === "object" && text(executable.name) &&
|
|
82
|
+
(executable.path === null || text(executable.path)) && (executable.realpath === null || text(executable.realpath)) &&
|
|
83
|
+
(executable.sha256 === null || HASH.test(executable.sha256)) &&
|
|
84
|
+
(executable.mode === undefined || Number.isSafeInteger(executable.mode) && executable.mode >= 0) &&
|
|
85
|
+
(executable.runtime === undefined || text(executable.runtime))))));
|
|
76
86
|
const matches = criteria.length > 0 && criteria.length === value.measurement.criterion_ids.length &&
|
|
77
87
|
criteria.every((criterion) => criterion.target === value.measurement.target &&
|
|
78
88
|
criterion.entrypoint === value.measurement.entrypoint && criterion.workload === value.measurement.workload &&
|
|
@@ -206,7 +216,7 @@ export function reduceGoalFlight(records) {
|
|
|
206
216
|
state = { ...state, no_progress_results: 0, replan_required: false, replan_used: false };
|
|
207
217
|
replanRevision = null;
|
|
208
218
|
}
|
|
209
|
-
requireState(state.phase === "active" && !state.replan_required, "transition", "Dispatch requires
|
|
219
|
+
requireState(state.phase === "active" && (!state.replan_required || state.replan_used), "transition", "Dispatch requires an active goal and the initial no-progress diagnosis.");
|
|
210
220
|
requireState(state.budget !== null && state.consumed_units + state.outstanding_reservations.length < state.budget.max_units, "budget", "Goal unit budget exhausted.");
|
|
211
221
|
requireState(state.budget.time_ms === null || (state.consumed_time_ms !== null && state.consumed_time_ms < state.budget.time_ms), "budget", "Goal time budget is exhausted or usage is unknown.");
|
|
212
222
|
requireState(state.budget.cost_usd === null || (state.consumed_cost_usd !== null && state.consumed_cost_usd < state.budget.cost_usd), "budget", "Goal cost budget is exhausted or usage is unknown.");
|
|
@@ -342,7 +352,7 @@ export function reduceGoalFlight(records) {
|
|
|
342
352
|
event.receipt.terminal_revision === state.revision && event.receipt.acceptance_fingerprint === state.acceptance_fingerprint &&
|
|
343
353
|
event.receipt.unit_ids.every((id) => state.unit_ids.includes(id)) && event.receipt.session_ids.every((id) => state.session_ids.includes(id)) &&
|
|
344
354
|
event.receipt.evidence_refs.every((id) => state.evidence_refs.includes(id)) &&
|
|
345
|
-
(event.receipt.status !== "succeeded" || (event.receipt.stop_reason === "completed" && allAccepted
|
|
355
|
+
(event.receipt.status !== "succeeded" || (event.receipt.stop_reason === "completed" && allAccepted)) &&
|
|
346
356
|
(event.receipt.status !== "stopped" || event.receipt.stop_reason !== "completed"), "transition", "Terminal receipt is not bound to settled goal state.");
|
|
347
357
|
state = { ...state, phase: event.receipt.status === "succeeded" ? "terminal" : "stopped",
|
|
348
358
|
stop_reason: event.receipt.stop_reason, tickets: [], receipt: event.receipt };
|
|
@@ -236,6 +236,8 @@ export interface OperatorMission {
|
|
|
236
236
|
consultations?: MissionConsultation[];
|
|
237
237
|
/** Host-observed current-Mission Worker attempts; only an implementation failure followed by a failed remediation can qualify for Rescue. */
|
|
238
238
|
attempts?: MissionAttempt[];
|
|
239
|
+
/** Native mutating-tool observations, not inference from a role name or a write estimate. */
|
|
240
|
+
sourceAuthors?: string[];
|
|
239
241
|
/** One bounded Astra Rescue per eligible unit; validation, independent review and root acceptance remain separate. */
|
|
240
242
|
rescue?: MissionTerminalRescue;
|
|
241
243
|
/** A cancelled run replaced by a later real user turn; never reuse its acceptance or evidence. */
|
|
@@ -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 {
|
|
@@ -755,7 +759,8 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
|
|
|
755
759
|
interpretation: "Compare original requests with the submitted candidate and actual evidence. Historical PASS is not current PASS. Inspect concrete gaps, not routine archive searches or full source rereads. Existing completion and Review guards still apply." };
|
|
756
760
|
}
|
|
757
761
|
export function missionReviewIndependent(mission, child) {
|
|
758
|
-
return child !== undefined && !
|
|
762
|
+
return child !== undefined && !mission.sourceAuthors?.includes(child) &&
|
|
763
|
+
!(mission.corrections ?? []).some(correction => correction.author === child);
|
|
759
764
|
}
|
|
760
765
|
export function missionPacket(mission, run) {
|
|
761
766
|
const predecessor = run && mission.runID !== run.runID && (mission.supersededRunID === run.runID || run.phase === "cancelled") ? run : undefined;
|
|
@@ -803,14 +808,14 @@ export function missionPacket(mission, run) {
|
|
|
803
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."
|
|
804
809
|
: run?.phase === "awaiting-acceptance" && mission.corrections?.some(item => item.runID === run.runID && item.status === "ready") && !reviewAccepted
|
|
805
810
|
? mission.corrections.find(item => item.runID === run.runID)?.selfRecheck?.unresolvedFindings.length
|
|
806
|
-
? "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."
|
|
807
812
|
: mission.corrections.find(item => item.runID === run.runID)?.selfRecheck?.residualMajor
|
|
808
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."
|
|
809
|
-
: "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."
|
|
810
815
|
: mission.kind === "operation" && operationStatus === "running"
|
|
811
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."
|
|
812
817
|
: run?.phase === "awaiting-decision" && mission.corrections?.some(item => item.runID === run.runID && item.status === "failed")
|
|
813
|
-
? "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."
|
|
814
819
|
: run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
|
|
815
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."
|
|
816
821
|
: run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && !unit.normalRemediationUsed)
|
|
@@ -821,8 +826,8 @@ export function missionPacket(mission, run) {
|
|
|
821
826
|
: run?.phase === "awaiting-acceptance" && !operationComplete
|
|
822
827
|
? `Requested operation is ${operationStatus}. Continue the actual operation or report its blocker; auxiliary checks and review disposition cannot complete it.`
|
|
823
828
|
: run?.phase === "awaiting-acceptance" ? (reviewAccepted
|
|
824
|
-
? "Coordinator: review permits submission. Retain
|
|
825
|
-
: "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.")
|
|
826
831
|
: "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
|
|
827
832
|
}
|
|
828
833
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
@@ -280,6 +280,8 @@ export declare class OperatorRuntime {
|
|
|
280
280
|
private file;
|
|
281
281
|
/** Exact persisted source for read-only status projections. */
|
|
282
282
|
statePath(root: string): string;
|
|
283
|
+
/** Stable detail source for a settled run replaced by Mission replanning. */
|
|
284
|
+
replanArchivePath(state: OperatorState): string;
|
|
283
285
|
/** Read-only lineage projection. Never imports archived evidence into current acceptance. */
|
|
284
286
|
acceptanceHistory(state: OperatorState, missionRunIDs: readonly string[]): Promise<{
|
|
285
287
|
status: "available" | "unavailable";
|
|
@@ -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 = [];
|
|
@@ -791,10 +795,17 @@ export class OperatorRuntime {
|
|
|
791
795
|
if (JSON.stringify(commands) === JSON.stringify(unit.unit.validation) &&
|
|
792
796
|
JSON.stringify(directories) === JSON.stringify(priorDirectories))
|
|
793
797
|
return state;
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
const now = new Date().toISOString();
|
|
797
|
-
|
|
798
|
+
// Keep unaffected native checks in this admission. Only a new command or a
|
|
799
|
+
// changed execution directory needs proof after this registration repair.
|
|
800
|
+
const registeredAt = { ...unit.reviewerCorrection.validationRegisteredAt }, now = new Date().toISOString();
|
|
801
|
+
const previousCommands = new Set(unit.unit.validation.map(normalizeCommand));
|
|
802
|
+
for (const command of commands) {
|
|
803
|
+
if (!previousCommands.has(command) ||
|
|
804
|
+
resolve(this.projectRoot, directories[command] ?? ".") !== resolve(this.projectRoot, priorDirectories[command] ?? ".")) {
|
|
805
|
+
registeredAt[command] = now;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
unit.reviewerCorrection.validationRegisteredAt = registeredAt;
|
|
798
809
|
if (unit.directExecution)
|
|
799
810
|
unit.directExecution.validationCwd = directories;
|
|
800
811
|
state.units[state.units.indexOf(unit)] = { ...unit, unit: { ...unit.unit, validation: commands, validation_cwd: directories } };
|
|
@@ -871,14 +882,12 @@ export class OperatorRuntime {
|
|
|
871
882
|
if (!previous || previous.runID !== mission.replaceRunID || ["cancelled", "completed"].includes(previous.phase)) {
|
|
872
883
|
throw new Error("mission-replan-run-mismatch");
|
|
873
884
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
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"))
|
|
878
888
|
throw new Error("mission-replan-worker-still-active");
|
|
879
|
-
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);
|
|
880
890
|
if (mission.reviewerCorrection && ((!failedContinuation && (previous.phase !== "awaiting-acceptance" || previous.units.some(unit => unit.status !== "succeeded"))) ||
|
|
881
|
-
previous.units.some(unit => unit.reviewerCorrection && unit.reviewerCorrection.author !== mission.reviewerCorrection.author) ||
|
|
882
891
|
plan.units.length !== 1 || plan.acceptance.length !== previous.acceptance.length ||
|
|
883
892
|
previous.acceptance.some((text, i) => text !== plan.acceptance[i]) ||
|
|
884
893
|
JSON.stringify(plan.acceptance_proof) !== JSON.stringify(previous.acceptanceProof) ||
|
|
@@ -1159,10 +1168,11 @@ export class OperatorRuntime {
|
|
|
1159
1168
|
await handle.close();
|
|
1160
1169
|
}
|
|
1161
1170
|
}
|
|
1162
|
-
if ((superseding || replanning) && previous) {
|
|
1171
|
+
if ((superseding || replanning || previous?.phase === "completed") && previous) {
|
|
1163
1172
|
// A crash before the state switch may leave a checkpoint while the old run later advances.
|
|
1164
1173
|
// Sequence-qualified replan snapshots keep that history without blocking a subsequent correction.
|
|
1165
|
-
const archive =
|
|
1174
|
+
const archive = replanning || previous.phase === "completed" ? this.replanArchivePath(previous)
|
|
1175
|
+
: `${this.file(root)}.${previous.runID}.archive`;
|
|
1166
1176
|
try {
|
|
1167
1177
|
const handle = await open(archive, "wx", 0o600);
|
|
1168
1178
|
created.push(archive);
|
|
@@ -1316,8 +1326,6 @@ export class OperatorRuntime {
|
|
|
1316
1326
|
return args.task_id === expected.task_id && args.subagent_type === expected.subagent_type &&
|
|
1317
1327
|
args.description === expected.description && args.prompt === expected.prompt;
|
|
1318
1328
|
}
|
|
1319
|
-
if (unit.reviewerCorrection && (args.model !== undefined || args.variant !== undefined))
|
|
1320
|
-
return false;
|
|
1321
1329
|
if (unit.reviewerCorrection ? args.task_id !== unit.reviewerCorrection.author : args.task_id !== undefined && args.task_id !== "")
|
|
1322
1330
|
return false;
|
|
1323
1331
|
if (args.subagent_type !== unit.task.subagent_type || args.description !== unit.task.description)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { normalizeWorktreeScopePath,
|
|
1
|
+
import { normalizeWorktreeScopePath, worktreeScopesConflict } from "./worktree-scope.js";
|
|
2
2
|
const MESSAGES = {
|
|
3
3
|
duplicate: "Task, worktree, branch, and artifact identities must be unique.",
|
|
4
4
|
unknownDependency: "Dependency must name another task in this contract.",
|
|
5
5
|
cycle: "Dependency graph must be acyclic.",
|
|
6
6
|
mode: "Mode, worker bound, and task count are inconsistent.",
|
|
7
7
|
path: "Scope and changed paths must be normalized repository-relative paths.",
|
|
8
|
-
overlap: "Parallel tasks must not have write
|
|
8
|
+
overlap: "Parallel tasks must not have overlapping write scopes. Exploratory reads are concurrent.",
|
|
9
9
|
artifact: "Commit artifact must match its task, base, and declared write scope.",
|
|
10
10
|
failure: "Failure task and fallback must satisfy the fixed failure policy.",
|
|
11
11
|
};
|
|
@@ -99,8 +99,7 @@ export function validateWorktreeParallelContract(contract) {
|
|
|
99
99
|
const right = entries[rightIndex];
|
|
100
100
|
if (right.scopes === undefined)
|
|
101
101
|
continue;
|
|
102
|
-
const overlaps = left.scopes
|
|
103
|
-
right.scopes.write.some((path) => left.scopes.read.some((other) => worktreeScopesOverlap(path, other)));
|
|
102
|
+
const overlaps = worktreeScopesConflict(left.scopes, right.scopes);
|
|
104
103
|
if (overlaps)
|
|
105
104
|
diagnostics.push(issue("WTP006_SCOPE_OVERLAP", `/tasks/${right.index}/scope`, MESSAGES.overlap));
|
|
106
105
|
}
|
|
@@ -18,6 +18,7 @@ export function worktreeScopesOverlap(left, right) {
|
|
|
18
18
|
return left === "**" || right === "**" || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
19
19
|
}
|
|
20
20
|
export function worktreeScopesConflict(left, right) {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Exploration is not an exclusive resource. Validation checks input freshness;
|
|
22
|
+
// write ownership alone prevents concurrent mutation of the same target.
|
|
23
|
+
return left.write.some((path) => right.write.some((other) => worktreeScopesOverlap(path, other)));
|
|
23
24
|
}
|
package/dist/plugin/fast-lane.js
CHANGED
|
@@ -391,6 +391,11 @@ export class FastLaneController {
|
|
|
391
391
|
const retry = lineValue(prompt, "fallback_retry");
|
|
392
392
|
const basis = fallbackBasis(prompt);
|
|
393
393
|
const prompts = phase === "initial" ? candidate.initialPrompts : candidate.verificationPrompts;
|
|
394
|
+
// Prompt registration precedes native execution. Seeing it again can mean an
|
|
395
|
+
// unstarted dispatch retry or continuation of the same retained Reviewer.
|
|
396
|
+
// Native call ownership joins active work; this lineage cache is not a retry grant.
|
|
397
|
+
if (retry === undefined && (candidate.initialPrompts.has(basis) || candidate.verificationPrompts.has(basis)))
|
|
398
|
+
return;
|
|
394
399
|
if (phase === "initial" && candidate.verificationPrompts.size > 0) {
|
|
395
400
|
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
396
401
|
}
|
|
@@ -410,8 +415,6 @@ export class FastLaneController {
|
|
|
410
415
|
candidate.fallbackRetries.add(`${phase}\0${basis}`);
|
|
411
416
|
}
|
|
412
417
|
else {
|
|
413
|
-
if (prompts.has(basis))
|
|
414
|
-
throw new FastLaneDeniedError("CONSULTATION_RETRY_INVALID");
|
|
415
418
|
if (phase === "initial" && candidate.initialPrompts.size > 0) {
|
|
416
419
|
throw new FastLaneDeniedError("REVIEW_PHASE_INVALID");
|
|
417
420
|
}
|
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();
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface OpenCodePluginInput {
|
|
|
20
20
|
/** Native background Job ownership, persisted by the V2 adapter. */
|
|
21
21
|
nativeBackground?: {
|
|
22
22
|
awaiting(sessionID: string): Promise<boolean>;
|
|
23
|
+
reconcile?(sessionID: string): Promise<void>;
|
|
23
24
|
};
|
|
24
25
|
/** V2 adapter can project and restore native session-scoped correction permissions. */
|
|
25
26
|
reviewerCorrectionPermissions?: boolean;
|
|
@@ -66,6 +67,15 @@ export interface OpenCodeHooks {
|
|
|
66
67
|
reviewerCorrectionSessions?: (sessionID: string) => Promise<readonly string[]>;
|
|
67
68
|
/** Internal adapter transport; retains the existing profile owner across reload. */
|
|
68
69
|
backgroundOwner?: (callID: string, restore?: Record<string, unknown>) => Record<string, unknown> | undefined;
|
|
70
|
+
/** Read-only recovery of a retained host result; never repeats Task settlement or validation. */
|
|
71
|
+
backgroundResult?: (parent: string, callID: string, child: string, promptID?: string) => Promise<string | undefined>;
|
|
72
|
+
/** Native transport joins an already-running Task instead of submitting a duplicate prompt. */
|
|
73
|
+
nativeTaskJoin?: (callID: string, consume?: boolean) => {
|
|
74
|
+
sessionID: string;
|
|
75
|
+
callID: string;
|
|
76
|
+
output?: string;
|
|
77
|
+
status?: "running" | "completed";
|
|
78
|
+
} | undefined;
|
|
69
79
|
event?: (input: {
|
|
70
80
|
event: OpenCodeEvent;
|
|
71
81
|
}) => Promise<void>;
|
package/dist/plugin/index.js
CHANGED
|
@@ -1366,7 +1366,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1366
1366
|
? await input.runtimeBridge?.missionDispatchCall?.(authorization.rootSessionID, toolInput.sessionID, authorization.taskID) : undefined);
|
|
1367
1367
|
const correction = correcting && dispatchCallID && members
|
|
1368
1368
|
? { taskID: authorization.taskID, dispatchCallID, commands: members } : undefined;
|
|
1369
|
-
const admittedSnapshot = await protectedSnapshot(authorization).catch(() => undefined);
|
|
1369
|
+
const admittedSnapshot = await protectedSnapshot(authorization, { directory, command: args.command }).catch(() => undefined);
|
|
1370
1370
|
if (admittedSnapshot === undefined)
|
|
1371
1371
|
return;
|
|
1372
1372
|
const root = goalRoot(owner);
|
|
@@ -2316,14 +2316,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2316
2316
|
if (state.phase === "terminal" || state.receipt !== null) {
|
|
2317
2317
|
throw new Error("SORTIE_GOAL_CONTROL_DENIED: terminal");
|
|
2318
2318
|
}
|
|
2319
|
-
if (state.replan_required) {
|
|
2320
|
-
if (state.replan_used) {
|
|
2321
|
-
await terminalGoal(sessionID, "stop_no_progress", "stopped");
|
|
2322
|
-
throw new Error("SORTIE_GOAL_CONTROL_DENIED: stop_no_progress");
|
|
2323
|
-
}
|
|
2319
|
+
if (state.replan_required && !state.replan_used) {
|
|
2324
2320
|
state = await ledger.appendGoal({ kind: "goal.replanned", at: new Date().toISOString(),
|
|
2325
2321
|
goal_id: state.goal_id, revision: state.revision, reason: "no-progress" });
|
|
2326
2322
|
}
|
|
2323
|
+
// No-progress is diagnostic, not another implicit execution budget. Useful
|
|
2324
|
+
// repair can continue within the same explicit unit/time/cost limits below.
|
|
2327
2325
|
if (state.budget !== null && state.consumed_units + state.outstanding_reservations.length >= state.budget.max_units) {
|
|
2328
2326
|
await terminalGoal(sessionID, "stop_budget", "stopped");
|
|
2329
2327
|
throw budgetDenied("units");
|
|
@@ -5025,8 +5023,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5025
5023
|
for (const [ownerSessionID, authorization] of sessionAuthorizations) {
|
|
5026
5024
|
if (ownerSessionID === sessionID || authorization.suspended)
|
|
5027
5025
|
continue;
|
|
5028
|
-
for (const [requested, held, access] of [[writeScopes, authorization.writeScopes, "write/write"]
|
|
5029
|
-
[writeScopes, authorization.readScopes, "write/read"], [readScopes, authorization.writeScopes, "read/write"]]) {
|
|
5026
|
+
for (const [requested, held, access] of [[writeScopes, authorization.writeScopes, "write/write"]]) {
|
|
5030
5027
|
for (const left of requested)
|
|
5031
5028
|
for (const right of held) {
|
|
5032
5029
|
if (conflicts.length < 8 && writeScopesOverlap([left], [right])) {
|
|
@@ -73,6 +73,7 @@ export declare function missionReviewValidation(run: OperatorState, statePath: s
|
|
|
73
73
|
export declare function observedMissionValidation(validation: readonly string[], childSessionID: string | null, history: readonly Record<string, unknown>[], notBefore?: number, directory?: string, directories?: Readonly<Record<string, string>>): {
|
|
74
74
|
attempts: readonly {
|
|
75
75
|
command: string;
|
|
76
|
+
native_command?: string;
|
|
76
77
|
directory?: string;
|
|
77
78
|
observed_members?: readonly string[];
|
|
78
79
|
exit_code: number | null;
|