sortie-dogs 0.13.14 → 0.13.16
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 +2 -1
- package/dist/core/operator-runtime.js +11 -4
- 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/index.d.ts +10 -0
- package/dist/plugin/index.js +5 -8
- package/dist/plugin/mission-review.js +105 -56
- package/dist/plugin/native-background.d.ts +6 -1
- package/dist/plugin/native-background.js +60 -2
- package/dist/plugin/profiled.js +212 -46
- package/dist/plugin/protected-snapshot.d.ts +23 -1
- package/dist/plugin/protected-snapshot.js +209 -5
- package/dist/plugin/v2.js +109 -20
- package/dist/runtime-assets.d.ts +2 -2
- package/dist/runtime-assets.js +2 -2
- package/dist/runtime-mission-assets.d.ts +3 -0
- package/dist/runtime-mission-assets.js +17 -7
- 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.16](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.16)**
|
|
23
|
+
· [Release notes](docs/release-v0.13.16.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.16
|
|
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.16
|
|
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.16**. 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.16-practical-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.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.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.16-practical-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.16-practical-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. */
|
|
@@ -755,7 +755,8 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
|
|
|
755
755
|
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
756
|
}
|
|
757
757
|
export function missionReviewIndependent(mission, child) {
|
|
758
|
-
return child !== undefined && !
|
|
758
|
+
return child !== undefined && !mission.sourceAuthors?.includes(child) &&
|
|
759
|
+
!(mission.corrections ?? []).some(correction => correction.author === child);
|
|
759
760
|
}
|
|
760
761
|
export function missionPacket(mission, run) {
|
|
761
762
|
const predecessor = run && mission.runID !== run.runID && (mission.supersededRunID === run.runID || run.phase === "cancelled") ? run : undefined;
|
|
@@ -791,10 +791,17 @@ export class OperatorRuntime {
|
|
|
791
791
|
if (JSON.stringify(commands) === JSON.stringify(unit.unit.validation) &&
|
|
792
792
|
JSON.stringify(directories) === JSON.stringify(priorDirectories))
|
|
793
793
|
return state;
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
const now = new Date().toISOString();
|
|
797
|
-
|
|
794
|
+
// Keep unaffected native checks in this admission. Only a new command or a
|
|
795
|
+
// changed execution directory needs proof after this registration repair.
|
|
796
|
+
const registeredAt = { ...unit.reviewerCorrection.validationRegisteredAt }, now = new Date().toISOString();
|
|
797
|
+
const previousCommands = new Set(unit.unit.validation.map(normalizeCommand));
|
|
798
|
+
for (const command of commands) {
|
|
799
|
+
if (!previousCommands.has(command) ||
|
|
800
|
+
resolve(this.projectRoot, directories[command] ?? ".") !== resolve(this.projectRoot, priorDirectories[command] ?? ".")) {
|
|
801
|
+
registeredAt[command] = now;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
unit.reviewerCorrection.validationRegisteredAt = registeredAt;
|
|
798
805
|
if (unit.directExecution)
|
|
799
806
|
unit.directExecution.validationCwd = directories;
|
|
800
807
|
state.units[state.units.indexOf(unit)] = { ...unit, unit: { ...unit.unit, validation: commands, validation_cwd: directories } };
|
|
@@ -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/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])) {
|
|
@@ -12,7 +12,7 @@ import { taskChildSessionID } from "./task-result-repair.js";
|
|
|
12
12
|
import { normalizeManifestScope } from "../core/path.js";
|
|
13
13
|
import { declaredArtifacts } from "./declared-artifacts.js";
|
|
14
14
|
import { canonicalDeclaredValidationMembers, normalizeCommand } from "./gate.js";
|
|
15
|
-
import { currentSnapshotProtection, isRuntimeControlPath, refreshProtectedSnapshot, snapshotScratchExclusion, validationInputSnapshot } from "./protected-snapshot.js";
|
|
15
|
+
import { currentSnapshotProtection, isRuntimeControlPath, refreshProtectedSnapshot, snapshotControlExclusion, snapshotScratchExclusion, validationInputSnapshot } from "./protected-snapshot.js";
|
|
16
16
|
const exec = promisify(execFile);
|
|
17
17
|
const notRepository = (error) => /fatal: not a git repository \(or (?:any of the parent directories|any parent up to mount point [^\r\n)]+)\)(?:: \.git)?/u
|
|
18
18
|
.test(String(error.stderr ?? ""));
|
|
@@ -113,6 +113,8 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
113
113
|
const declared = validation.map(normalizeCommand);
|
|
114
114
|
if (!Number.isFinite(notBefore))
|
|
115
115
|
return { ready: false, reason: "mission-review-correction-validation-admission-unavailable" };
|
|
116
|
+
if (!declared.length)
|
|
117
|
+
return { ready: false, reason: "mission-review-correction-validation-missing", nextOccurrence: 0 };
|
|
116
118
|
const attempts = [];
|
|
117
119
|
const seen = new Set();
|
|
118
120
|
let editedAt = notBefore;
|
|
@@ -150,56 +152,88 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
const matched = [];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
155
|
+
// Registration repairs replace only affected recipes. Retain the ordered actual
|
|
156
|
+
// occurrences of unchanged checks and join the changed checks after their own
|
|
157
|
+
// registration, rather than making an inserted/corrected command rerun its neighbors.
|
|
158
|
+
const groups = new Map();
|
|
159
|
+
for (const [occurrence, command] of declared.entries()) {
|
|
160
|
+
const registration = checks && registeredAt ? registeredAt[command] ?? "" : "";
|
|
161
|
+
const group = groups.get(registration) ?? [];
|
|
162
|
+
group.push(occurrence);
|
|
163
|
+
groups.set(registration, group);
|
|
164
|
+
}
|
|
165
|
+
const complete = [];
|
|
166
|
+
let incomplete;
|
|
167
|
+
for (const occurrences of groups.values()) {
|
|
168
|
+
const required = occurrences.map(index => declared[index]);
|
|
169
|
+
const partial = [];
|
|
170
|
+
let completed;
|
|
171
|
+
let previousEnd = notBefore, overlap = false, failed;
|
|
172
|
+
for (const attempt of attempts.sort((a, b) => a.started - b.started || a.completed - b.completed)) {
|
|
173
|
+
// Bound native checks are compared with their actual input bytes below. A later
|
|
174
|
+
// report/cache write is not itself a source change. Unbound history stays conservative.
|
|
175
|
+
if (!checks && attempt.started < editedAt)
|
|
176
|
+
continue;
|
|
177
|
+
const recorded = registeredAt && checks?.find(check => check.callID === attempt.callID && check.childSessionID === child);
|
|
178
|
+
const commands = recorded
|
|
179
|
+
? canonicalDeclaredValidationMembers(attempt.command, recorded.command)
|
|
180
|
+
: canonicalDeclaredValidationMembers(attempt.command, declared, occurrences[partial.length % required.length] ?? 0);
|
|
181
|
+
if (!commands || recorded && JSON.stringify(commands) !== JSON.stringify(recorded.command))
|
|
182
|
+
continue;
|
|
183
|
+
const members = commands.flatMap((command, member) => required.includes(command) &&
|
|
184
|
+
validationDirectoryMatches([command], attempt.directory, directory, directories) &&
|
|
185
|
+
(registeredAt?.[command] === undefined || Number.isFinite(Date.parse(registeredAt[command])) &&
|
|
186
|
+
attempt.started >= Date.parse(registeredAt[command])) ? [{ command, member }] : []);
|
|
187
|
+
if (!members.length || !registeredAt && members.length !== commands.length)
|
|
188
|
+
continue;
|
|
189
|
+
if (attempt.started < previousEnd) {
|
|
190
|
+
completed = undefined;
|
|
191
|
+
partial.length = 0;
|
|
192
|
+
failed = undefined;
|
|
193
|
+
overlap = true;
|
|
194
|
+
previousEnd = Math.max(previousEnd, attempt.completed);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
previousEnd = attempt.completed;
|
|
198
|
+
if (members[0].command !== required[partial.length] || partial.length === required.length)
|
|
199
|
+
partial.length = 0;
|
|
168
200
|
failed = undefined;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
matched.length = 0;
|
|
176
|
-
failed = undefined;
|
|
177
|
-
// A failed && chain cannot infer any member success (including commands before its failure).
|
|
178
|
-
if (attempt.exit !== 0) {
|
|
179
|
-
complete = undefined;
|
|
180
|
-
matched.length = 0;
|
|
181
|
-
failed = { command: commands, outcome: "fail", exitCode: attempt.exit };
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
for (const [member, command] of commands.entries()) {
|
|
185
|
-
if (command !== declared[matched.length]) {
|
|
186
|
-
matched.length = 0;
|
|
187
|
-
if (command !== declared[0])
|
|
188
|
-
continue;
|
|
201
|
+
// A failed && chain proves no member, even if earlier members could have passed.
|
|
202
|
+
if (attempt.exit !== 0) {
|
|
203
|
+
completed = undefined;
|
|
204
|
+
partial.length = 0;
|
|
205
|
+
failed = { command: commands, outcome: "fail", exitCode: attempt.exit };
|
|
206
|
+
continue;
|
|
189
207
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
208
|
+
for (const { member, command } of members) {
|
|
209
|
+
if (command !== required[partial.length]) {
|
|
210
|
+
partial.length = 0;
|
|
211
|
+
if (command !== required[0])
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
partial.push({ callID: attempt.callID, member, occurrence: occurrences[partial.length] });
|
|
215
|
+
if (partial.length === required.length) {
|
|
216
|
+
completed = [...partial];
|
|
217
|
+
overlap = false;
|
|
218
|
+
}
|
|
194
219
|
}
|
|
195
220
|
}
|
|
221
|
+
if (failed)
|
|
222
|
+
return { ready: false, reason: `mission-review-correction-validation-failed:${failed.command.join(" && ")}`, failure: failed, nextOccurrence: occurrences[0] };
|
|
223
|
+
if (!completed) {
|
|
224
|
+
const next = occurrences[partial.length] ?? occurrences[0];
|
|
225
|
+
if (!incomplete || next < incomplete.nextOccurrence)
|
|
226
|
+
incomplete = { ready: false,
|
|
227
|
+
reason: overlap && !partial.length ? "mission-review-correction-validation-order:overlap"
|
|
228
|
+
: `mission-review-correction-validation-missing:${declared[next]}`, nextOccurrence: next };
|
|
229
|
+
}
|
|
230
|
+
else
|
|
231
|
+
complete.push(...completed);
|
|
232
|
+
matched.push(...partial);
|
|
196
233
|
}
|
|
197
|
-
if (
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
return { ready: false, reason: "mission-review-correction-validation-order:overlap", nextOccurrence: 0 };
|
|
201
|
-
if (!complete)
|
|
202
|
-
return { ready: false, reason: `mission-review-correction-validation-missing:${declared[matched.length]}`, nextOccurrence: matched.length };
|
|
234
|
+
if (incomplete)
|
|
235
|
+
return incomplete;
|
|
236
|
+
complete.sort((a, b) => a.occurrence - b.occurrence);
|
|
203
237
|
for (const item of complete) {
|
|
204
238
|
const command = declared[item.occurrence];
|
|
205
239
|
const check = checks?.find(check => check.callID === item.callID && check.childSessionID === child);
|
|
@@ -394,14 +428,32 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
|
|
|
394
428
|
export async function missionReviewSource(directory, run, evidence = [], baseline, priorScope, displayBaseline = baseline) {
|
|
395
429
|
const scope = missionReviewScope(priorScope, run);
|
|
396
430
|
const bindings = scope.validationBindings ?? [];
|
|
397
|
-
const
|
|
431
|
+
const manifestScope = (path) => path === "." ? `${resolve(directory)}/**` : path;
|
|
432
|
+
const protection = await currentSnapshotProtection(directory, { read: scope.read.map(manifestScope), write: scope.write.map(manifestScope) });
|
|
398
433
|
const scratch = bindings.map(binding => ({ binding, excludes: snapshotScratchExclusion(binding, protection) }));
|
|
434
|
+
const controls = bindings.map(binding => ({ binding, excludes: snapshotControlExclusion(directory, binding, protection) }));
|
|
435
|
+
const scopes = [...scope.read, ...scope.write].map(path => resolve(directory, normalizeManifestScope(manifestScope(path)).path));
|
|
436
|
+
const broadOutput = scope.write.some(path => resolve(directory, normalizeManifestScope(manifestScope(path)).path) === resolve(directory));
|
|
437
|
+
const explicit = [...scopes, ...protection].filter(path => path !== resolve(directory));
|
|
438
|
+
const related = (root, path) => {
|
|
439
|
+
const rest = relative(root, path);
|
|
440
|
+
return rest === "" || rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute(rest);
|
|
441
|
+
};
|
|
399
442
|
const excluded = (path) => {
|
|
400
443
|
const absolute = resolve(directory, path);
|
|
401
444
|
const covers = (binding) => [...binding.source_paths, ...binding.candidate_paths].some(root => {
|
|
402
445
|
const rest = relative(resolve(directory, root), absolute);
|
|
403
446
|
return rest === "" || (rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute(rest));
|
|
404
447
|
});
|
|
448
|
+
const first = path.replaceAll("\\", "/").split("/")[0];
|
|
449
|
+
const config = (process.platform === "win32" ? first.toLowerCase() : first) === ".opencode";
|
|
450
|
+
if (broadOutput && (config || isRuntimeControlPath(path)) && !explicit.some(root => related(root, absolute) || related(absolute, root))) {
|
|
451
|
+
// An old snapshot cannot silently acquire the new config exclusion. Review
|
|
452
|
+
// uses the same saved recipe when bindings exist, including mixed old/new proof.
|
|
453
|
+
if (!config || !bindings.length || controls.some(item => covers(item.binding) && item.excludes(absolute)) &&
|
|
454
|
+
!controls.some(item => covers(item.binding) && !item.excludes(absolute)))
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
405
457
|
return scratch.some(item => item.excludes(absolute)) &&
|
|
406
458
|
!scratch.some(item => covers(item.binding) && !item.excludes(absolute));
|
|
407
459
|
};
|
|
@@ -507,16 +559,16 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
|
|
|
507
559
|
local.push(scoped || ".");
|
|
508
560
|
}
|
|
509
561
|
const toolInput = scope.read.some(path => normalizeManifestScope(path).path.startsWith(TOOL_ENVIRONMENT));
|
|
510
|
-
const
|
|
562
|
+
const gitScopes = [...local, ...(toolInput ? [] : [`:(exclude)${TOOL_ENVIRONMENT}`])];
|
|
511
563
|
const git = async (args) => (await exec("git", args, { cwd: directory, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
512
564
|
let names = "", ignored = "", repository = true, scopedDiff = "";
|
|
513
565
|
const untracked = new Set(), currentFiles = new Set();
|
|
514
566
|
try {
|
|
515
567
|
if (local.length) {
|
|
516
|
-
names = await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...
|
|
517
|
-
for (const path of (await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...
|
|
568
|
+
names = await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...gitScopes]);
|
|
569
|
+
for (const path of (await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...gitScopes])).split("\0").filter(Boolean))
|
|
518
570
|
untracked.add(path);
|
|
519
|
-
ignored = await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...
|
|
571
|
+
ignored = await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...gitScopes]);
|
|
520
572
|
}
|
|
521
573
|
}
|
|
522
574
|
catch (error) {
|
|
@@ -530,9 +582,6 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
|
|
|
530
582
|
if (walked.has(path) || excluded(path))
|
|
531
583
|
return;
|
|
532
584
|
walked.add(path);
|
|
533
|
-
if (local.includes(".") && isRuntimeControlPath(path) && !local.some(scope => scope !== "." &&
|
|
534
|
-
(path === scope || path.startsWith(`${scope}/`))))
|
|
535
|
-
return;
|
|
536
585
|
if (!toolInput && (path === TOOL_ENVIRONMENT || path.startsWith(`${TOOL_ENVIRONMENT}/`)))
|
|
537
586
|
return;
|
|
538
587
|
const absolute = resolve(directory, path);
|
|
@@ -592,7 +641,7 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
|
|
|
592
641
|
if (repository && local.length && displayBaseline) {
|
|
593
642
|
// HEAD-only diffs are empty once the author commits. Display the original mission delta
|
|
594
643
|
// or the focused correction delta without changing the full fingerprint basis.
|
|
595
|
-
const changed = (await git(["diff", "--name-only", "-z", "--no-ext-diff", displayBaseline, "--", ...
|
|
644
|
+
const changed = (await git(["diff", "--name-only", "-z", "--no-ext-diff", displayBaseline, "--", ...gitScopes]))
|
|
596
645
|
.split("\0").filter(path => path && !excluded(path));
|
|
597
646
|
const shown = changed.slice(0, 16);
|
|
598
647
|
const heading = changed.length ? `Changed since ${displayBaseline === baseline ? "mission" : "correction"} baseline (${displayBaseline}; ${changed.length} paths):\n` : "";
|
|
@@ -615,7 +664,7 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
|
|
|
615
664
|
omitted.push(...changed.slice(shown.length));
|
|
616
665
|
}
|
|
617
666
|
else if (repository && local.length) {
|
|
618
|
-
diff = await currentGitDiff(git,
|
|
667
|
+
diff = await currentGitDiff(git, gitScopes);
|
|
619
668
|
}
|
|
620
669
|
const unchanged = diff.length === 0;
|
|
621
670
|
let excerpt = selected + diff;
|
|
@@ -14,15 +14,18 @@ export interface NativeBackgroundDispatch {
|
|
|
14
14
|
launched: boolean;
|
|
15
15
|
terminal?: "succeeded" | "failed" | "interrupted";
|
|
16
16
|
settled?: boolean;
|
|
17
|
+
/** Host-reconciled output for this exact native invocation, not another completion prompt. */
|
|
18
|
+
result?: string;
|
|
17
19
|
}
|
|
18
20
|
/** Lifecycle transport for native Jobs, not a scheduler. The host owns execution and wakeup. */
|
|
19
21
|
export declare class NativeBackgroundLifecycle {
|
|
20
22
|
private readonly context;
|
|
21
23
|
private readonly settle;
|
|
22
24
|
private readonly owner?;
|
|
25
|
+
private readonly recoverResult?;
|
|
23
26
|
private readonly operations;
|
|
24
27
|
private readonly restored;
|
|
25
|
-
constructor(context: OpenCodeV2Context, settle: (dispatch: NativeBackgroundDispatch, text: string) => Promise<void>, owner?: ((callID: string, restore?: ObjectValue) => ObjectValue | undefined) | undefined);
|
|
28
|
+
constructor(context: OpenCodeV2Context, settle: (dispatch: NativeBackgroundDispatch, text: string) => Promise<string | void>, owner?: ((callID: string, restore?: ObjectValue) => ObjectValue | undefined) | undefined, recoverResult?: ((parent: string, callID: string, child: string, promptID?: string) => Promise<string | undefined>) | undefined);
|
|
26
29
|
private serial;
|
|
27
30
|
private read;
|
|
28
31
|
private write;
|
|
@@ -31,6 +34,8 @@ export declare class NativeBackgroundLifecycle {
|
|
|
31
34
|
launched(parent: string, callID: string, child: string | undefined, running: boolean): Promise<boolean>;
|
|
32
35
|
awaiting(sessionID: string): Promise<boolean>;
|
|
33
36
|
reconcile(parent: string): Promise<void>;
|
|
37
|
+
/** Add provenance beside its existing native notification, without editing history or waking the parent again. */
|
|
38
|
+
project(parent: string, messages: readonly unknown[]): Promise<unknown[]>;
|
|
34
39
|
/** Restore accounting before a resumed child produces further tool observations. */
|
|
35
40
|
resume(sessionID: string): Promise<void>;
|
|
36
41
|
checkpoint(sessionID: string): Promise<void>;
|