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 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.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.15
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.15
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.15**. SWE-bench remains an optional
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.15-native-identity-v1`; the unchanged Codex skill marker is
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
@@ -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.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;
@@ -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.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: {
@@ -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. */
@@ -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
- guidance: "Current original_requests/requirements govern; older plans remain historical. Continue from known decisions, environment, completed work and failures. 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 merely because ownership changed. Historical results are not fresh formal validation or new authority." } };
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 && !(mission.corrections ?? []).some(correction => correction.author === child);
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. Call repair_review for the SAME original correction owner; do not dispatch another Reviewer or accept unresolved Medium. Retain original requirements and cumulative spend."
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; call review_mission for the SAME native author to explicitly self-recheck the retained findings, relevant impact and original requirements. Only a concrete reachable Major risk remaining after self-recheck requires a DIFFERENT Reviewer. Known Major/Medium defects still require correction. Do not restart unchanged checks or investigation; CORRECTION_READY alone is not acceptance."
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. Call repair_review for a new admission in the SAME original Reviewer's native session; fix its own regression, rerun inherited checks and retain the commit/clean boundary and cumulative spend. Do not use ordinary retry, Rescue or a fresh Worker replan. Failure is never acceptance."
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 advisory review notes; do not repeat passed validation or review for evidence formatting. Operator performs final acceptance against the original requirements."
825
- : "Coordinator: correct known Major/Medium findings, then obtain explicit native author self-recheck (different Reviewer only for residual concrete Major risk), or initial independent review, then submit_mission. Operator compares all requirements with source/evidence before complete_mission.")
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
- // 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]));
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
- if (previous.units.some(unit => unit.reviewerCorrection) && !mission.reviewerCorrection) {
875
- throw new Error("mission-review-correction-owner-continuation-required");
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?.author === mission.reviewerCorrection?.author);
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 = `${this.file(root)}.${previous.runID}${replanning ? `.${previous.sequence}` : ""}.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, 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
  }
@@ -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. */
@@ -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();
@@ -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])) {
@@ -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;