sortie-dogs 0.13.15 → 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 CHANGED
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
19
19
  [![Node.js](https://img.shields.io/node/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
20
20
  [![MIT License](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
21
21
 
22
- **Current release: [v0.13.15](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.15)**
23
- · [Release notes](docs/release-v0.13.15.md)
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.15
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.15
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.15**. SWE-bench remains an optional
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.15-native-identity-v1`; the unchanged Codex skill marker is
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.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.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
@@ -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.15-native-identity-v1";
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;
@@ -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.15-native-identity-v1";
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: {
@@ -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 terminal handling or one bounded replan.");
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 && !state.replan_required)) &&
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 && !(mission.corrections ?? []).some(correction => correction.author === child);
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
- // This changes only the check recipe, not the admitted Task, contract or spend.
795
- // Keep every prior attempt, but require actual native proof after registration.
796
- const now = new Date().toISOString();
797
- unit.reviewerCorrection.validationRegisteredAt = Object.fromEntries(commands.map(command => [command, now]));
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, worktreeScopesOverlap } from "./worktree-scope.js";
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/write or read/write scope overlap.",
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.write.some((path) => [...right.scopes.write, ...right.scopes.read].some((other) => worktreeScopesOverlap(path, other))) ||
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
- return left.write.some((path) => [...right.write, ...right.read].some((other) => worktreeScopesOverlap(path, other))) ||
22
- right.write.some((path) => left.read.some((other) => worktreeScopesOverlap(path, other)));
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
  }
@@ -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
  }
@@ -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>;
@@ -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
- let complete;
154
- let previousEnd = notBefore, overlap = false, failed;
155
- for (const attempt of attempts.sort((a, b) => a.started - b.started || a.completed - b.completed)) {
156
- // Bound native checks are compared with their actual input bytes below. A later
157
- // report/cache write is not itself a source change. Unbound history stays conservative.
158
- if (!checks && attempt.started < editedAt)
159
- continue;
160
- const commands = canonicalDeclaredValidationMembers(attempt.command, declared, matched.length % declared.length);
161
- if (!commands || !validationDirectoryMatches(commands, attempt.directory, directory, directories) ||
162
- commands.some(command => registeredAt?.[command] !== undefined &&
163
- (!Number.isFinite(Date.parse(registeredAt[command])) || attempt.started < Date.parse(registeredAt[command]))))
164
- continue;
165
- if (attempt.started < previousEnd) {
166
- complete = undefined;
167
- matched.length = 0;
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
- overlap = true;
170
- previousEnd = Math.max(previousEnd, attempt.completed);
171
- continue; // A later complete ordered retry can recover in this same admission.
172
- }
173
- previousEnd = attempt.completed;
174
- if (commands[0] !== declared[matched.length] || matched.length === declared.length)
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
- matched.push({ callID: attempt.callID, member, occurrence: matched.length });
191
- if (matched.length === declared.length) {
192
- complete = [...matched];
193
- overlap = false;
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 (failed)
198
- return { ready: false, reason: `mission-review-correction-validation-failed:${failed.command.join(" && ")}`, failure: failed, nextOccurrence: 0 };
199
- if (overlap && !matched.length)
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 protection = bindings.some(binding => binding.freshness) ? await currentSnapshotProtection(directory, scope) : [];
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 scopes = [...local, ...(toolInput ? [] : [`:(exclude)${TOOL_ENVIRONMENT}`])];
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", "--", ...scopes]);
517
- for (const path of (await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...scopes])).split("\0").filter(Boolean))
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", "--", ...scopes]);
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, "--", ...scopes]))
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, scopes);
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>;