sortie-dogs 0.14.0 → 0.14.2

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.14.0](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.0)**
23
- · [Release notes](docs/release-v0.14.0.md) · [Execution vision](docs/v0.14.0-execution-vision.md)
22
+ **Current release: [v0.14.2](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.2)**
23
+ · [Release notes](docs/release-v0.14.2.md) · [Execution vision](docs/v0.14.0-execution-vision.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.14.0
60
+ npm install --save-dev sortie-dogs@0.14.2
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.14.0
89
+ npm install --save-dev sortie-dogs@0.14.2
90
90
  npx --no-install sortie-dogs init .
91
91
  ```
92
92
 
@@ -178,6 +178,30 @@ general speed or success-rate guarantee.
178
178
 
179
179
  ### SWE-bench evaluation
180
180
 
181
+ #### v0.14.0 dev23 (2026-10-15 UTC)
182
+
183
+ **Official resolution: 10/23 (43.48%); Mission completion: 22/23.** One pass through the normal
184
+ OpenCode CLI + Sortie Mission path, using the fixed released v0.14.0 package.
185
+
186
+ - All 23 frozen patches were officially scored: **10 resolved, 13 unresolved**, zero empty patches,
187
+ evaluation errors or infrastructure failures. The cost-stopped `sqlfluff__sqlfluff-2419` patch
188
+ resolved officially, but its Mission remains incomplete; official correctness does not imply acceptance.
189
+ - Pinned public Lite dev rows; one inference attempt per task, no retries. Official grading:
190
+ `split=dev`, `max_workers=1`, `retry_count=0`. Inference concurrency was **1** due to low host memory.
191
+ - Actual Operator/Coordinator/Reviewer model: `openai/gpt-6.1-sol#xhigh`;
192
+ Worker: `openai/gpt-6-luna-fast#max`. Ten cases used Workers; 13 used Coordinator direct execution.
193
+ - Inference wall time **2h53m28s**; separate official grading **7m19s**.
194
+ - Known estimated inference cost **$16.76**, with one pending usage record. Separate holds:
195
+ **$2** for unresolved usage and **$5** for implementation/supervision; these are not known expenses.
196
+ Costs are API-equivalent estimates, not invoices; the $2/task monitor is not an exact billing cap.
197
+ - Release/runner commit `e69a3b2317a76eeea1229034f52902812ff1d580`;
198
+ package SHA-256 `120922c31dd6d85040210d68456192b4e6aa45dc87a11451476f3ed576a68e30`.
199
+
200
+ Local evidence (generated artifacts are not committed):
201
+ `_testenv/autodev/sortie-dogs-dev23/benchmark-v0140-20261015/one-pass-summary.json`.
202
+
203
+ #### Historical v0.12.24 test300
204
+
181
205
  **SWE-bench Lite test300: 170/300 (56.67%)**, fixed **v0.12.24**, one pass@1 campaign.
182
206
  Nine empty patches, zero official evaluation errors, frozen predictions and trajectories for all
183
207
  300 instances. Workers ran Luna Fast/max; management/review ran GPT-6 Sol/xhigh.
@@ -195,8 +219,8 @@ known expense. Official local evaluation and leaderboard acceptance are separate
195
219
  **8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
196
220
  official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
197
221
 
198
- All scores belong to their fixed candidates, **not v0.14.0**. SWE-bench remains an optional
199
- measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
222
+ Each score belongs to its fixed candidate. SWE-bench remains an optional measurement, not a release
223
+ gate. Different splits, budgets, models and conditions are not controlled comparisons or proof of a fix's effect.
200
224
 
201
225
  ## Configuration
202
226
 
@@ -218,7 +242,7 @@ Install the desired package version and rerun **your host's** initializer:
218
242
 
219
243
  An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
220
244
  update it. [Global/update instructions](docs/configuration.md#global-availability).
221
- The current Mission asset marker is `0.14.0-autonomous-routing-v1`; the unchanged Codex skill marker is
245
+ The current Mission asset marker is `0.14.2-cold-resume-v1`; the unchanged Codex skill marker is
222
246
  `0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
223
247
 
224
248
  There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
@@ -234,7 +258,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
234
258
  [SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
235
259
  - **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
236
260
  [Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
237
- - **Releases:** [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
261
+ - **Releases:** [v0.14.2](docs/release-v0.14.2.md) · [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
238
262
  [v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
239
263
 
240
264
  ## 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.14.0-autonomous-routing-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.14.2-cold-resume-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.14.0-autonomous-routing-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.14.2-cold-resume-v1";
7
7
  export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
@@ -71,6 +71,9 @@ export function codexMissionProgress(event) {
71
71
  native_status: event.params.turn?.status };
72
72
  if (event.method === "sortie/continuation")
73
73
  return { thread_id: event.threadId, turn_id: event.params.turnId, phase: event.params.phase, calls: event.params.calls };
74
+ if (event.method === "sortie/coordinatorRestored")
75
+ return { thread_id: event.threadId, phase: "coordinator-restored", mission_id: event.params.missionId,
76
+ coordinator_id: event.params.coordinatorId, call_id: event.params.callId, native_outcome: event.params.outcome, next_action: event.params.action };
74
77
  if (event.method === "sortie/modelRoute")
75
78
  return { thread_id: event.threadId, phase: "model-route", agent: event.params.agent, model: event.params.model,
76
79
  effort: event.params.effort, service_tier: event.params.serviceTier };
@@ -79,8 +79,9 @@ export declare class CodexMissionSession {
79
79
  close(): Promise<void>;
80
80
  private claimOwner;
81
81
  private rootOf;
82
+ private restoreCoordinator;
82
83
  private recoverNotStarted;
83
- private bindWorkerDispatch;
84
+ private bindTaskDispatch;
84
85
  private recoverCompletedWorker;
85
86
  private required;
86
87
  private executeHostCommand;
@@ -62,6 +62,7 @@ export class CodexMissionSession {
62
62
  this.observations = missions.find(item => item.root === resume)?.codexUsage ?? [];
63
63
  await this.restoreSession(resume, "dog-operator");
64
64
  this.root = resume;
65
+ await this.restoreCoordinator(resume);
65
66
  await this.claimOwner(resume);
66
67
  await this.hooks.tool?.sortie_v010_operator_status?.execute({ view: "full" }, { sessionID: resume, agent: "dog-operator" });
67
68
  }
@@ -118,6 +119,45 @@ export class CodexMissionSession {
118
119
  current = this.required(current.parentID);
119
120
  return current.id;
120
121
  }
122
+ async restoreCoordinator(root) {
123
+ const missions = new OperatorMissionRuntime(this.directory, profile);
124
+ const mission = await missions.required(root);
125
+ if (!mission.coordinator || ["completed", "cancelled"].includes(mission.phase))
126
+ return;
127
+ if (!await codexOwnerGone(mission.codexOwner))
128
+ throw new Error("codex-mission-owner-active:no-resend");
129
+ // Codex can persist claim() and the child turn but omit the interrupted parent
130
+ // Task item. The existing Mission binding still owns this same Coordinator.
131
+ const coordinator = await this.restoreSession(mission.coordinator, "dogs-coordinator", root);
132
+ if (coordinator.parentID !== root || coordinator.agent !== "dogs-coordinator")
133
+ throw new Error("Retained Coordinator lineage mismatch.");
134
+ if (mission.dispatchOpen && mission.callID) {
135
+ // A retained terminal parent receipt already has the shared reconciliation path.
136
+ // The new binding is required only when that exact receipt is missing.
137
+ const parentReceipts = this.required(root).history.flatMap(message => Array.isArray(message.parts) ? message.parts.filter(object) : [])
138
+ .filter(part => part.type === "tool" && part.tool === "task" && part.callID === mission.callID && object(part.state) &&
139
+ ["completed", "error"].includes(String(part.state.status)) && object(part.state.input) &&
140
+ part.state.input.subagent_type === coordinator.agent &&
141
+ (part.state.input.task_id === undefined || part.state.input.task_id === coordinator.id));
142
+ if (parentReceipts.length === 1)
143
+ return;
144
+ const binding = mission.codexCoordinatorDispatch;
145
+ if (!binding || binding.missionID !== mission.id || binding.threadID !== root || binding.callID !== mission.callID ||
146
+ binding.childThreadID !== coordinator.id || binding.ownerGeneration !== mission.codexOwner?.generation ||
147
+ coordinator.history.filter(message => object(message.info) && message.info.role === "user" &&
148
+ message.info.id === binding.clientUserMessageID).length !== 1)
149
+ throw new Error("Retained Coordinator has unproven execution for the current dispatch; no resend. Reconcile its exact native turn before resuming.");
150
+ await missions.codexRecovery(root, async (current) => {
151
+ if (current.codexOwner?.generation !== binding.ownerGeneration || !await codexOwnerGone(current.codexOwner) ||
152
+ JSON.stringify(current.codexCoordinatorDispatch) !== JSON.stringify(binding))
153
+ throw new Error("codex-mission-recovery-generation-stale");
154
+ await missions.reconcileFinishedDispatch(root, mission.id, mission.callID);
155
+ });
156
+ await this.options.onEvent?.({ method: "sortie/coordinatorRestored", threadId: root,
157
+ params: { missionId: mission.id, coordinatorId: coordinator.id, callId: mission.callID,
158
+ outcome: coordinator.outcome, action: "resume-existing-task" } });
159
+ }
160
+ }
121
161
  async recoverNotStarted(threadID, turn, item) {
122
162
  if (item.tool !== "task" || !object(item.arguments) || item.arguments.task_id || typeof item.id !== "string" || typeof turn.id !== "string" ||
123
163
  !["completed", "interrupted", "failed"].includes(String(turn.status)))
@@ -156,7 +196,18 @@ export class CodexMissionSession {
156
196
  }
157
197
  return false;
158
198
  }
159
- async bindWorkerDispatch(parent, child, call, messageID) {
199
+ async bindTaskDispatch(parent, child, call, messageID) {
200
+ if (child.agent === "dogs-coordinator") {
201
+ await new OperatorMissionRuntime(this.directory, profile).update(this.rootOf(parent), state => {
202
+ if (state.root !== call.threadId || state.coordinator !== child.id || state.callID !== call.callId || !state.dispatchOpen ||
203
+ state.codexOwner?.generation !== this.owner.generation || state.codexOwner.closed)
204
+ throw new Error("codex-mission-dispatch-owner-stale");
205
+ state.codexCoordinatorDispatch = { missionID: state.id, threadID: call.threadId, turnID: call.turnId, callID: call.callId,
206
+ inputHash: createHash("sha256").update(JSON.stringify(call.arguments)).digest("hex"),
207
+ childThreadID: child.id, clientUserMessageID: messageID, ownerGeneration: this.owner.generation };
208
+ });
209
+ return;
210
+ }
160
211
  if (child.agent !== "dog-worker-v010" || !object(call.arguments) || call.arguments.task_id)
161
212
  return;
162
213
  const root = this.rootOf(parent), missions = new OperatorMissionRuntime(this.directory, profile);
@@ -365,7 +416,7 @@ export class CodexMissionSession {
365
416
  throw new Error(`Codex thread ${id} has no matching repository history.`);
366
417
  const turns = thread.turns.filter(object);
367
418
  for (const turn of turns) {
368
- if (!Array.isArray(turn.items) || turn.status === "inProgress" || turn.itemsView !== "full")
419
+ if (!Array.isArray(turn.items) || !["completed", "failed", "interrupted", "cancelled"].includes(String(turn.status)) || turn.itemsView !== "full")
369
420
  throw new Error(`Codex thread ${id} still has unproven execution; no resend. Reconcile its native execution before resuming.`);
370
421
  for (const item of turn.items.filter(object)) {
371
422
  // A terminal parent turn does not prove that its native executions or children stopped.
@@ -373,7 +424,8 @@ export class CodexMissionSession {
373
424
  ["commandExecution", "fileChange", "mcpToolCall"].includes(String(item.type)) &&
374
425
  !["completed", "failed", ...(item.type === "mcpToolCall" ? [] : ["declined"])].includes(String(item.status)))
375
426
  throw new Error(`Codex thread ${id} has an unresolved native execution; no resend. Reconcile its native execution before resuming.`);
376
- if (item.type !== "dynamicToolCall" || item.status === "completed" && (turn.status === "completed" || item.success === true))
427
+ if (item.type !== "dynamicToolCall" || ["completed", "failed"].includes(String(item.status)) &&
428
+ (turn.status === "completed" || item.success === true))
377
429
  continue;
378
430
  const unique = turns.flatMap(entry => Array.isArray(entry.items) ? entry.items.filter(object) : []).filter(entry => entry.id === item.id).length === 1;
379
431
  if (!unique)
@@ -475,7 +527,7 @@ export class CodexMissionSession {
475
527
  result = await this.promptTurn(session, "The previous native response ended while existing host tools were still running. Those exact calls have now returned:\n" +
476
528
  JSON.stringify(result.deferredToolResults) +
477
529
  "\nContinue this same Mission from these results through the remaining validation, delivery, review and root acceptance. " +
478
- "Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", undefined, true);
530
+ "Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", session.agent === "dogs-coordinator" ? onDispatch : undefined, true);
479
531
  }
480
532
  return result;
481
533
  }
@@ -577,7 +629,7 @@ export class CodexMissionSession {
577
629
  });
578
630
  await this.hooks["tool.execute.after"]?.({ tool: call.tool, sessionID: session.id, callID: call.callId, args }, { output: "", metadata: { sessionId: child.id, status: "running" } });
579
631
  const result = await this.prompt(child, String(args.prompt), async (messageID) => {
580
- await this.bindWorkerDispatch(session, child, call, messageID);
632
+ await this.bindTaskDispatch(session, child, call, messageID);
581
633
  dispatched = true;
582
634
  });
583
635
  text = result.finalResponse ?? "";
@@ -49,6 +49,7 @@ export interface CodexWorkerDispatch {
49
49
  clientUserMessageID: string;
50
50
  ownerGeneration: string;
51
51
  }
52
+ export type CodexCoordinatorDispatch = Omit<CodexWorkerDispatch, "runID" | "unitID">;
52
53
  export interface CodexCompletedTaskProof {
53
54
  dispatch: CodexWorkerDispatch;
54
55
  childTurnID: string;
@@ -209,6 +210,8 @@ export interface OperatorMission {
209
210
  codexNotStarted?: CodexNotStartedProof[];
210
211
  /** Missing parent receipts reconciled from exact completed native Worker turns. */
211
212
  codexCompletedTasks?: CodexCompletedTaskProof[];
213
+ /** Current Coordinator dispatch bound before its exact native turn is sent. */
214
+ codexCoordinatorDispatch?: CodexCoordinatorDispatch;
212
215
  version: "0.12";
213
216
  id: string;
214
217
  root: string;
@@ -319,6 +322,11 @@ export interface OperatorMission {
319
322
  admittedAt: number;
320
323
  reviewIdentity: string;
321
324
  };
325
+ /** Exact terminal of an unused inline grant; no implementation calls followed admission. */
326
+ noWriteReceipt?: {
327
+ promptID: string;
328
+ messageID: string;
329
+ };
322
330
  status: "prepared" | "running" | "ready" | "failed" | "cancelled";
323
331
  selfRecheck?: MissionSelfRecheck;
324
332
  }[];
@@ -891,7 +891,8 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
891
891
  }
892
892
  export function missionReviewIndependent(mission, child) {
893
893
  return child !== undefined && !mission.sourceAuthors?.includes(child) &&
894
- !(mission.corrections ?? []).some(correction => correction.author === child);
894
+ !(mission.corrections ?? []).some(correction => correction.author === child &&
895
+ !(correction.status === "failed" && correction.noWriteReceipt));
895
896
  }
896
897
  export function missionPacket(mission, run, includeWorkQuantity = true) {
897
898
  const predecessor = run && mission.runID !== run.runID && (mission.supersededRunID === run.runID || run.phase === "cancelled") ? run : undefined;
@@ -199,6 +199,14 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
199
199
  continue;
200
200
  }
201
201
  previousEnd = attempt.completed;
202
+ // A final-check-only refresh must replace its old binding, not force the
203
+ // unchanged prefix to run again. Repeated declarations stay ambiguous;
204
+ // preserve their existing occurrence rules and validate every saved input.
205
+ if (completed && partial.length === required.length && members.length === 1 &&
206
+ members[0].command === required.at(-1) && required.indexOf(members[0].command) === required.length - 1) {
207
+ partial.splice(0, partial.length, ...completed.slice(0, -1));
208
+ completed = undefined;
209
+ }
202
210
  if (members[0].command !== required[partial.length] || partial.length === required.length)
203
211
  partial.length = 0;
204
212
  failed = undefined;
@@ -366,7 +374,7 @@ export async function missionDeliveryObservation(directory, runID) {
366
374
  }
367
375
  /** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
368
376
  export function initialMissionReviewPrompt(mission, prompts) {
369
- return [mission.review?.initialPrompt, ...prompts].find(prompt => typeof prompt === "string" &&
377
+ return [mission.review?.initialPrompt, ...(mission.corrections ?? []).map(item => item.initialPrompt), ...prompts].find(prompt => typeof prompt === "string" &&
370
378
  prompt.startsWith(`candidate_id: ${mission.id}\n`) && /^review_phase: initial$/mu.test(prompt) &&
371
379
  /^canonical_validation_exit: 0$/mu.test(prompt));
372
380
  }
@@ -377,8 +385,9 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
377
385
  return [];
378
386
  // The completion hook has already bound this prompt to a real independent Reviewer child.
379
387
  // Avoid a second V2 history read when its page/list API is temporarily unavailable.
380
- if (initialMissionReviewPrompt(mission, []))
381
- return [mission.review.initialPrompt];
388
+ const initial = initialMissionReviewPrompt(mission, []);
389
+ if (initial)
390
+ return [initial];
382
391
  const owner = mission.coordinator ?? root;
383
392
  if (mission.coordinator !== null) {
384
393
  const coordinator = await host.get(owner);
@@ -355,6 +355,47 @@ export function createProfiledPlugin(profile, assetVersion) {
355
355
  return who.role === "dog-reviewer" && who.parent === (mission.coordinator ?? root);
356
356
  }
357
357
  async function restoreReviewAuthorship(root, mission, id) {
358
+ // An inline correction admission can end without any implementation: the
359
+ // controller may be the designated writer. Recover that exact native return
360
+ // rather than permanently treating the unused grant as source authorship.
361
+ const unused = mission.sourceAuthors?.includes(id) ? [] : (mission.corrections ?? []).filter(item => item.author === id && item.inlineReview && item.status === "failed" && !item.noWriteReceipt);
362
+ if (unused.length) {
363
+ const history = await reviewMessages(id);
364
+ const receipts = unused.flatMap(correction => {
365
+ const inline = correction.inlineReview;
366
+ const prompt = history.findIndex(message => record(message.info) && message.info.id === inline.promptID &&
367
+ message.info.role === "user" && message.info.sessionID === id);
368
+ if (prompt < 0)
369
+ return [];
370
+ const response = reviewResponseMessages(history, prompt);
371
+ const last = [...response].reverse().find(message => record(message.info) && message.info.role === "assistant");
372
+ const info = record(last?.info) ? last.info : undefined;
373
+ if (!info || info.sessionID !== id || info.finish !== "stop" || info.error || typeof info.id !== "string" ||
374
+ !record(info.time) || typeof info.time.completed !== "number" || info.time.completed < inline.admittedAt)
375
+ return [];
376
+ const parts = response.flatMap(message => Array.isArray(message.parts) ? message.parts.filter(record) : []);
377
+ const admitted = parts.findIndex(part => part.type === "tool" && part.tool === repairReview && record(part.state) &&
378
+ part.state.status === "completed" && record(part.state.time) && typeof part.state.time.end === "number" &&
379
+ part.state.time.end >= inline.admittedAt);
380
+ // Before admission this Reviewer was read-only. After it, require the
381
+ // observed terminal and only read/search calls; unknown tools or failed
382
+ // writes cannot establish that no implementation occurred.
383
+ if (admitted < 0 || parts.slice(admitted + 1).some(part => part.type === "tool" &&
384
+ (!["read", "grep", "glob"].includes(String(part.tool)) || !record(part.state) ||
385
+ !["completed", "error"].includes(String(part.state.status)))))
386
+ return [];
387
+ return [{ runID: correction.runID, promptID: inline.promptID, messageID: info.id }];
388
+ });
389
+ if (receipts.length)
390
+ mission = await missions.update(root, current => {
391
+ for (const receipt of receipts) {
392
+ const correction = current.corrections?.find(item => item.runID === receipt.runID && item.author === id &&
393
+ item.status === "failed" && item.inlineReview?.promptID === receipt.promptID);
394
+ if (correction)
395
+ correction.noWriteReceipt = { promptID: receipt.promptID, messageID: receipt.messageID };
396
+ }
397
+ });
398
+ }
358
399
  if (!missionReviewIndependent(mission, id) || !mission.attempts?.some(attempt => attempt.childSessionID === id))
359
400
  return mission;
360
401
  const history = await reviewMessages(id);
@@ -1187,6 +1228,9 @@ export function createProfiledPlugin(profile, assetVersion) {
1187
1228
  return { ...packet, next_action: "Fast-lane: dispatch the exact Reviewer Task from review_mission if not yet active; otherwise wait for that Reviewer. Do not start a Coordinator or another Worker while review is pending." };
1188
1229
  }
1189
1230
  if (mission.runID === run?.runID && run?.units.some(unit => unit.status === "running")) {
1231
+ if (mission.coordinator && !mission.dispatchOpen && !["completed", "cancelled"].includes(mission.phase))
1232
+ return { ...packet, task: missions.task(mission),
1233
+ next_action: "Resume this existing Coordinator Task with its task_id to continue the retained unit. Do not dispatch a replacement or wait for background work that has already ended." };
1190
1234
  return { ...packet, next_action: "The current unit is active. Continue its owning session; do not dispatch a replacement Worker or Coordinator. Native background completion will return to the parent." };
1191
1235
  }
1192
1236
  if (mission.coordinator === null && mission.runID === run?.runID && !mission.dispatchOpen &&
@@ -2834,7 +2878,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2834
2878
  }
2835
2879
  async function declareMissionUnits(root, actor, mission, raw, reason, execution, planningCallID, handoffNotes, workQuantities) {
2836
2880
  await control.currentBudget(root);
2837
- mission = await missions.required(root);
2881
+ mission = actor === root ? await reconcileMissionDispatch(root) ?? await missions.required(root) : await missions.required(root);
2838
2882
  let previous = await operators.read(root);
2839
2883
  if (sourceReconciliationRequired(mission, previous))
2840
2884
  return JSON.stringify({ ...missionDispatchPacket(mission, previous),
@@ -2849,8 +2893,34 @@ export function createProfiledPlugin(profile, assetVersion) {
2849
2893
  Array.isArray(execution.commands) && execution.commands.length === 0);
2850
2894
  const plan = missionPlan(mission, raw, input.directory);
2851
2895
  assertMissionWritePaths(mission, plan.units.flatMap(unit => unit.write));
2852
- if (actor === root && (mission.coordinator !== null || plan.units.length !== 1))
2853
- throw new Error("mission-coordinator-required: dispatch the returned Coordinator task");
2896
+ if (actor === root && (mission.coordinator !== null || plan.units.length !== 1)) {
2897
+ // A submitted Coordinator still owns corrective planning. Return its actual
2898
+ // continuation instead of referring to a Task that the rejection never supplied.
2899
+ // This is routing advice only: no replacement plan, reservation or admission.
2900
+ // Routing below alone chooses whether to offer a Task. A resumable parent
2901
+ // packet can already contain one even while its Reviewer is still pending.
2902
+ const { task: _dispatchTask, ...packet } = missionDispatchPacket(mission, previous);
2903
+ const reviewPending = mission.runID === previous?.runID && mission.review?.verdict === "pending";
2904
+ const active = mission.dispatchOpen || reviewPending || mission.runID === previous?.runID &&
2905
+ previous?.units.some(unit => unit.status === "running");
2906
+ const continuation = !active ? missions.task(mission) : undefined;
2907
+ // Existing Coordinator resumes accept a concrete prompt. Do not use the opaque
2908
+ // initial reference here: claim() would expand it from unchanged Mission state
2909
+ // and lose the unadmitted correction that prompted this continuation.
2910
+ const task = continuation && mission.coordinator !== null ? { ...continuation,
2911
+ prompt: `Continue the same Mission ${mission.id} in this existing Coordinator session, in the user's language. ` +
2912
+ "Retain the saved original requests, requirements, candidate, checks and cumulative budget.\n" +
2913
+ "Proposed correction from Operator root (not admitted; preserve requirements, candidate and cumulative budget):\n" +
2914
+ JSON.stringify({ units: raw, reason, execution, handoff_context: handoffNotes, work_quantities: workQuantities }) } : continuation;
2915
+ return JSON.stringify({ ...packet,
2916
+ status: active ? "mission-coordinator-active" : "mission-coordinator-required",
2917
+ ...(task ? { task } : {}),
2918
+ next_action: reviewPending
2919
+ ? "Dispatch the exact Reviewer Task from review_mission if not yet active; otherwise wait for that Reviewer. Do not start a Coordinator or another Worker while review is pending. The proposed plan has not been admitted."
2920
+ : active
2921
+ ? "Continue the active owning session or wait for its native completion; do not dispatch a duplicate Coordinator or Worker. The proposed plan has not been admitted."
2922
+ : "Dispatch this exact Task to continue the same Mission and Coordinator session with the concrete correction and retained checks. The proposed plan has not been admitted; keep requirements, candidate and cumulative budget unchanged." });
2923
+ }
2854
2924
  const same = previous?.planHash === createHash("sha256").update(JSON.stringify(plan)).digest("hex");
2855
2925
  const replanning = previous && !["completed", "cancelled"].includes(previous.phase) && !same;
2856
2926
  const activeDirect = previous?.units.find(unit => unit.status === "running" && unit.directExecution?.actor === actor);
@@ -3034,7 +3104,10 @@ export function createProfiledPlugin(profile, assetVersion) {
3034
3104
  if (args.executor === "worker" && args.units === undefined)
3035
3105
  return handoffDirect(root, context.sessionID, context.callID, args.handoff_context);
3036
3106
  const planned = await declareMissionUnits(root, context.sessionID, mission, args.units, args.reason, args.execution, context.callID, args.handoff_context, args.work_quantities);
3037
- return args.executor === "self" && record(JSON.parse(planned).task) ? startDirect(root, context.sessionID) : planned;
3107
+ const task = JSON.parse(planned).task;
3108
+ // A Coordinator continuation is not a newly prepared direct unit.
3109
+ return args.executor === "self" && record(task) && task.subagent_type !== profileAgent(profile, "dog-operator")
3110
+ ? startDirect(root, context.sessionID) : planned;
3038
3111
  });
3039
3112
  }
3040
3113
  catch (error) {
@@ -3178,7 +3251,7 @@ export function createProfiledPlugin(profile, assetVersion) {
3178
3251
  current.verdict = "findings";
3179
3252
  current.result = findings;
3180
3253
  // The independent investigation is recorded by this real tool call, not a fictitious terminal.
3181
- current.initialPrompt = current.task.prompt;
3254
+ current.initialPrompt = initialMissionReviewPrompt(item, [current.task.prompt]);
3182
3255
  });
3183
3256
  review = mission.review;
3184
3257
  }
@@ -3375,7 +3448,7 @@ export function createProfiledPlugin(profile, assetVersion) {
3375
3448
  // turn. Generate verification only when a completed initial review is recoverable;
3376
3449
  // otherwise generate an admissible initial review instead of trapping the Coordinator.
3377
3450
  const prior = mission.review?.task?.prompt;
3378
- const completed = risk.length > 0 && prior && mission.review?.child && !mission.review.initialPrompt
3451
+ const completed = risk.length > 0 && prior && !initialMissionReviewPrompt(mission, [])
3379
3452
  ? await completedMissionReviewPrompts(mission, profile, root, prior, {
3380
3453
  get: async (id) => payload(await session("get", { path: { id }, query: { directory: input.directory } })),
3381
3454
  messages: reviewMessages,
@@ -18,6 +18,8 @@ export declare const MISSION_REFLECTION_WORKFLOW = "When bounded process reflect
18
18
  export declare function missionOperatorContent(profile: RuntimeProfile, version: string): string;
19
19
  /** Keep representation oracles consistent in direct execution, Worker and Review. */
20
20
  export declare const MISSION_REPRESENTATION_ORACLE = "Same-type representation, not name/visitor analogy; resolve conflicts.";
21
+ /** Planning and Review use public type families instead of only issue-named examples. */
22
+ export declare const MISSION_TYPE_FAMILY_ORACLE = "When changing a dispatch/exclusion set, derive the affected same-representation family from an existing\nschema/registry and its public callers, not just issue examples. Compare sibling entries under that\ncontract; reuse suitable parameterized checks. Do not widen unrelated families or require an exhaustive\nmatrix, another unit or review lane.";
21
23
  /** Coordinator preserves source-backed return semantics in direct work or a Worker's objective. */
22
24
  export declare const MISSION_RETURN_CONTRACT = "For a changed value-producing operation, derive its return contract from public callers and unchanged source/tests.\nPreserve established return type/container, metadata and label alignment unless the original request changes them;\nequal numeric values alone do not prove that contract. Trace which inputs determine result shape and labels,\nincluding non-primary parameters; a scalar primary input need not yield a scalar result. For a mask or conditional\nreplacement, preserve established broadcasting and missing-value propagation; an unknown condition is not proof\nthat replacement is intended. Carry the affected contract into direct work or the Worker's objective.\nReuse an adequate focused assertion; add only the smallest source-backed regression when needed, not an input matrix,\nanother unit, validation lane or review.";
23
25
  /** Coordinator preserves source-backed mutation semantics in direct work or a Worker's objective. */
@@ -25,7 +27,7 @@ export declare const MISSION_MUTATION_CONTRACT = "For a changed mutating operati
25
27
  /** Coordinator authors and plans regressions without adding another validation/review lane. */
26
28
  export declare const MISSION_REGRESSION_PLACEMENT = "Prefer an existing relevant test suite/subtest or fixture when it can express the regression assertion.\nIf a new test module is needed, prefer a distinctive behavior-specific name over automatically mirroring the implementation module.\nPreserve established assertions and project test discovery. This is placement guidance, not another test, unit or review requirement.";
27
29
  /** Shared by the installed Reviewer and its host-generated mission prompt. */
28
- export declare const MISSION_BEHAVIOR_REVIEW = "Establish the test oracle independently of the patch: tests added with the implementation are claims to\nreview, not established API behavior. For a changed failure path, compare a pre-existing analogous public\ntest and its shared assertion helper, including default expected outputs, with the new case. A new test\nthat asserts the implementation's current result can encode the defect rather than catch it. Preserve\nthe pre-change contract unless the request changes it; do not invent a universal failure-result convention.\nFor changed observable behavior, inspect affected public caller assertions/output snapshots found by search, not only implementation-local fixtures.\nReconcile expected outputs with the original requirements and reuse adequate checks; no extra review round.\nFor requested side-effect changes, check remaining controls with existing client-owned state, not only isolated defaults.\nJudge allowed effects from the original request; preserve authorized opt-in behavior.\nSame-type representation, not name/visitor analogy; resolve conflicts.\nTrace a concrete rejected input through the changed code back to the public caller, checking result,\nerror and observable state together, including earlier results that can survive failure. Missing assertions\nalone are not defects; a concrete contradiction with the established contract is a finding even if tests pass.\nDuring correction, reuse the established public test harness where applicable and exercise the smallest\nregression exposing the defect before the fix and passing afterward. Do not change expected values or\nhelper defaults merely to agree with the implementation; derive them from the original contract.\nPrefer existing suites/subtests for regressions. New test entry/helper names should be distinctive\nand compose with other same-package test files, not create generic package-level collisions.\n\nFor a requested per-call mode/option, trace where the setting is chosen and where changed code uses\nretained objects or closures. Distinguish the current caller's setting from a captured creation-time\nsetting; use the requested contract to decide which governs. If actual source crosses that boundary\nand existing tests do not cover it, exercise its smallest public case, including the disabled behavior.\nDo not invent a lifecycle matrix or infer that an opt-in feature is correct from only its enabled path.\n\nFor a changed failure handler, inspect the operation it calls and the public inputs reaching it,\nincluding failures not listed in the new handler. Use the supplied source/tests and established API behavior\nto identify a concrete input that could still violate the requested contract. A passing normal input does\nnot settle a different failure outcome of that same operation. A demonstrable material defect is FINDINGS; ask for\nan excerpt or result only when a specific material outcome cannot be settled. Do not invent new behavior,\nrequire an exhaustive exception inventory, or recommend catching every exception.\n\nReport FINDINGS only for concrete major or medium defects with a material impact on the original requirements,\npublic behavior, correctness or required validation. Name the consequence and smallest necessary fix.\nDo not turn minor style, wording, optional improvements or speculative edge cases into FINDINGS or EVIDENCE_GAPS.\nUse read/search to settle missing context in the current review. If a consequential uncertainty remains,\nrecord EVIDENCE_GAPS as an advisory limitation, not a demand for another evidence packet. Missing narration,\nrequirement mappings, hashes or clipped excerpts alone never justify a blocking finding.\n\nAssess behavioral requirements against actual source, tests and observed results. A required check known to\nhave failed or not run is a concrete finding; missing documentation of a check is not proof it did not run.\nFor incidental workflow constraints such as cache settings or command-path spelling, use existing host observations;\nabsence of a separate settings dump or historical log is not itself an evidence gap. Flag observed material\ncontradictions. The Operator owns final comparison with the original request. If a missing check genuinely\naffects correctness or a requested deliverable, name that consequence and the smallest useful next check.\nOn verification, compare the correction and its affected public outcomes with the original contract, not\nonly the wording of previous findings. Reuse unchanged evidence; do not expand into optional improvements\nor an exhaustive input matrix. PASS means no material finding, not exhaustive proof of every path.";
30
+ export declare const MISSION_BEHAVIOR_REVIEW = "Establish the test oracle independently of the patch: tests added with the implementation are claims to\nreview, not established API behavior. For a changed failure path, compare a pre-existing analogous public\ntest and its shared assertion helper, including default expected outputs, with the new case. A new test\nthat asserts the implementation's current result can encode the defect rather than catch it. Preserve\nthe pre-change contract unless the request changes it; do not invent a universal failure-result convention.\nFor changed observable behavior, inspect affected public caller assertions/output snapshots found by search, not only implementation-local fixtures.\nReconcile expected outputs with the original requirements and reuse adequate checks; no extra review round.\nFor requested side-effect changes, check remaining controls with existing client-owned state, not only isolated defaults.\nJudge allowed effects from the original request; preserve authorized opt-in behavior.\nSame-type representation, not name/visitor analogy; resolve conflicts.\nWhen changing a dispatch/exclusion set, derive the affected same-representation family from an existing\nschema/registry and its public callers, not just issue examples. Compare sibling entries under that\ncontract; reuse suitable parameterized checks. Do not widen unrelated families or require an exhaustive\nmatrix, another unit or review lane.\nTrace a concrete rejected input through the changed code back to the public caller, checking result,\nerror and observable state together, including earlier results that can survive failure. Missing assertions\nalone are not defects; a concrete contradiction with the established contract is a finding even if tests pass.\nDuring correction, reuse the established public test harness where applicable and exercise the smallest\nregression exposing the defect before the fix and passing afterward. Do not change expected values or\nhelper defaults merely to agree with the implementation; derive them from the original contract.\nPrefer existing suites/subtests for regressions. New test entry/helper names should be distinctive\nand compose with other same-package test files, not create generic package-level collisions.\n\nFor a requested per-call mode/option, trace where the setting is chosen and where changed code uses\nretained objects or closures. Distinguish the current caller's setting from a captured creation-time\nsetting; use the requested contract to decide which governs. If actual source crosses that boundary\nand existing tests do not cover it, exercise its smallest public case, including the disabled behavior.\nDo not invent a lifecycle matrix or infer that an opt-in feature is correct from only its enabled path.\n\nFor a changed failure handler, inspect the operation it calls and the public inputs reaching it,\nincluding failures not listed in the new handler. Use the supplied source/tests and established API behavior\nto identify a concrete input that could still violate the requested contract. A passing normal input does\nnot settle a different failure outcome of that same operation. A demonstrable material defect is FINDINGS; ask for\nan excerpt or result only when a specific material outcome cannot be settled. Do not invent new behavior,\nrequire an exhaustive exception inventory, or recommend catching every exception.\n\nReport FINDINGS only for concrete major or medium defects with a material impact on the original requirements,\npublic behavior, correctness or required validation. Name the consequence and smallest necessary fix.\nDo not turn minor style, wording, optional improvements or speculative edge cases into FINDINGS or EVIDENCE_GAPS.\nUse read/search to settle missing context in the current review. If a consequential uncertainty remains,\nrecord EVIDENCE_GAPS as an advisory limitation, not a demand for another evidence packet. Missing narration,\nrequirement mappings, hashes or clipped excerpts alone never justify a blocking finding.\n\nAssess behavioral requirements against actual source, tests and observed results. A required check known to\nhave failed or not run is a concrete finding; missing documentation of a check is not proof it did not run.\nFor incidental workflow constraints such as cache settings or command-path spelling, use existing host observations;\nabsence of a separate settings dump or historical log is not itself an evidence gap. Flag observed material\ncontradictions. The Operator owns final comparison with the original request. If a missing check genuinely\naffects correctness or a requested deliverable, name that consequence and the smallest useful next check.\nOn verification, compare the correction and its affected public outcomes with the original contract, not\nonly the wording of previous findings. Reuse unchanged evidence; do not expand into optional improvements\nor an exhaustive input matrix. PASS means no material finding, not exhaustive proof of every path.";
29
31
  /** Mission Reviewer uses ordinary host read/search permissions, not a supplied-packet-only protocol. */
30
32
  export declare function missionReviewerContent(profile: RuntimeProfile): string;
31
33
  export declare function missionCoordinatorContent(profile: RuntimeProfile, version: string): string;
@@ -284,6 +284,11 @@ existing user authorization and project gates; npm publication remains manual.
284
284
  }
285
285
  /** Keep representation oracles consistent in direct execution, Worker and Review. */
286
286
  export const MISSION_REPRESENTATION_ORACLE = `Same-type representation, not name/visitor analogy; resolve conflicts.`;
287
+ /** Planning and Review use public type families instead of only issue-named examples. */
288
+ export const MISSION_TYPE_FAMILY_ORACLE = `When changing a dispatch/exclusion set, derive the affected same-representation family from an existing
289
+ schema/registry and its public callers, not just issue examples. Compare sibling entries under that
290
+ contract; reuse suitable parameterized checks. Do not widen unrelated families or require an exhaustive
291
+ matrix, another unit or review lane.`;
287
292
  /** Coordinator preserves source-backed return semantics in direct work or a Worker's objective. */
288
293
  export const MISSION_RETURN_CONTRACT = `For a changed value-producing operation, derive its return contract from public callers and unchanged source/tests.
289
294
  Preserve established return type/container, metadata and label alignment unless the original request changes them;
@@ -319,6 +324,7 @@ For changed observable behavior, inspect affected public caller assertions/outpu
319
324
  Reconcile expected outputs with the original requirements and reuse adequate checks; no extra review round.
320
325
  ${MISSION_CLIENT_STATE_CHECK}
321
326
  ${MISSION_REPRESENTATION_ORACLE}
327
+ ${MISSION_TYPE_FAMILY_ORACLE}
322
328
  Trace a concrete rejected input through the changed code back to the public caller, checking result,
323
329
  error and observable state together, including earlier results that can survive failure. Missing assertions
324
330
  alone are not defects; a concrete contradiction with the established contract is a finding even if tests pass.
@@ -540,6 +546,7 @@ If peer source/tests expose a plausible related still-unhandled error, probe its
540
546
  before choosing the fix; reuse adequate checks. Include this in the current author's objective/check
541
547
  plan, not another unit or review round. Do not invent an exhaustive error matrix.
542
548
  ${MISSION_REPRESENTATION_ORACLE}
549
+ ${MISSION_TYPE_FAMILY_ORACLE}
543
550
  ${MISSION_RETURN_CONTRACT}
544
551
  ${MISSION_MUTATION_CONTRACT}
545
552
  ${MISSION_REGRESSION_PLACEMENT}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",