sortie-dogs 0.14.5 → 0.14.7

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.
@@ -32,7 +32,7 @@ Use the natural-language Mission route on POSIX and Windows:
32
32
 
33
33
  Forward `--model`, `--effort`, or `--permissions` only when the user selected those values. Never silently widen the native permission profile. If an existing Mission is reported, resume only its exact root with `--resume`; do not launch replacement work.
34
34
 
35
- Leave `--model` unset to retain role-specific defaults: Sol 6.1 Operator/Coordinator/Reviewer and Luna-fast Worker. The Codex adapter maps the shared Luna-fast alias to native `gpt-6-luna` plus the `priority` (Fast) service tier; it does not alter OpenCode defaults. Fast consumes subscription limits faster than Standard; no metered API fallback is allowed.
35
+ Leave `--model` unset to retain role-specific defaults: Sol 6.1 Operator/Coordinator/Reviewer and ordinary Terra 6.1 Standard/max Worker (no Fast/priority). Scout and dedicated Luna routes remain unchanged. The Codex adapter retains the shared Luna-fast alias mapping to native `gpt-6-luna` plus the `priority` (Fast) service tier. Fast consumes subscription limits faster than Standard; no metered API fallback is allowed.
36
36
 
37
37
  On Windows the same Mission command resolves existing PowerShell 7 automatically. Add `--trusted-pwsh <absolute-pwsh.exe>` only when an exact existing executable needs to be selected; do not add a separate shell preflight or approval step.
38
38
 
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.5](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.5)**
23
- · [Release notes](docs/release-v0.14.5.md) · [Execution vision](docs/v0.14.0-execution-vision.md)
22
+ **Current release: [v0.14.7](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.7)**
23
+ · [Release notes](docs/release-v0.14.7.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.5
60
+ npm install --save-dev sortie-dogs@0.14.7
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.5
89
+ npm install --save-dev sortie-dogs@0.14.7
90
90
  npx --no-install sortie-dogs init .
91
91
  ```
92
92
 
@@ -144,9 +144,12 @@ one. Required success checks, missing commands and active/unstarted operations s
144
144
  ## Default models
145
145
 
146
146
  - **Operator / Coordinator / Reviewer / Advisor:** GPT-6.1 Sol, `xhigh`.
147
- - **Worker / Scout:** Luna Fast, `max`.
148
- - **OpenCode:** `openai/gpt-6.1-sol#xhigh` and `openai/gpt-6-luna-fast#max`.
149
- - **Codex:** native `gpt-6.1-sol` / `xhigh`; the Luna-fast alias maps to native
147
+ - **Worker:** GPT-6.1 Terra Standard, `max` (no Fast/priority).
148
+ - **Scout / dedicated Luna Worker:** Luna Fast, `max`, unchanged.
149
+ - **OpenCode:** ordinary Worker `openai/gpt-6.1-terra#max`; management/review
150
+ `openai/gpt-6.1-sol#xhigh`.
151
+ - **Codex:** ordinary Worker native `gpt-6.1-terra` / `max`, Standard; management/review
152
+ `gpt-6.1-sol` / `xhigh`. The retained Luna-fast alias maps to native
150
153
  `gpt-6-luna` / `max` with separate `serviceTier: "priority"` (Fast).
151
154
 
152
155
  Explicit host model selections remain authoritative. OpenCode routing and Codex native settings
@@ -242,8 +245,8 @@ Install the desired package version and rerun **your host's** initializer:
242
245
 
243
246
  An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
244
247
  update it. [Global/update instructions](docs/configuration.md#global-availability).
245
- The current Mission asset marker is `0.14.5-unborn-proof-v1`; the unchanged Codex skill marker is
246
- `0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
248
+ The current Mission asset marker is `0.14.7-visible-recovery-v1`; the Codex skill marker is
249
+ `0.14.6-codex-skill-v1`. Installed markers alone do not prove a running host reloaded the new version.
247
250
 
248
251
  There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
249
252
  follow the [manual removal guide](docs/uninstall.md), never delete the entire `.opencode` directory.
@@ -258,9 +261,14 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
258
261
  [SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
259
262
  - **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
260
263
  [Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
261
- - **Releases:** [v0.14.5](docs/release-v0.14.5.md) · [v0.14.4](docs/release-v0.14.4.md) · [v0.14.3](docs/release-v0.14.3.md) · [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) ·
264
+ - **Releases:** [v0.14.7](docs/release-v0.14.7.md) · [v0.14.6](docs/release-v0.14.6.md) · [v0.14.5](docs/release-v0.14.5.md) · [v0.14.4](docs/release-v0.14.4.md) · [v0.14.3](docs/release-v0.14.3.md) · [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) ·
262
265
  [v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
263
266
 
267
+ ## Support
268
+
269
+ [GitHub Sponsors](https://github.com/sponsors/zufall-upon) — sponsorships become available
270
+ once the maintainer's Sponsors profile is approved and published.
271
+
264
272
  ## Community
265
273
 
266
274
  [Contributing](CONTRIBUTING.md) · [Code of conduct](CODE_OF_CONDUCT.md) ·
@@ -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.5-unborn-proof-v1";
7
- export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.14.7-visible-recovery-v1";
7
+ export declare const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
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.5-unborn-proof-v1";
7
- export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.14.7-visible-recovery-v1";
7
+ export const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
@@ -1,5 +1,5 @@
1
1
  import type { CodexTurnEvent } from "../codex/app-server.js";
2
- export declare const CODEX_MISSION_USAGE = "Usage: sortie-dogs codex mission --prompt <text>\n [--resume <root-thread-id>] [--project-root <path>] [--executable <codex>] [--model <model>] [--effort <effort>] [--permissions <native-profile>] [--trusted-pwsh <absolute-pwsh.exe>]\n\nRuns the existing Mission Operator, Coordinator, Worker and Reviewer through Codex.\n--model and --effort explicitly override all roles; otherwise packaged role defaults apply.\n--permissions explicitly selects an existing native profile; omission retains native defaults.\nWindows uses existing PowerShell 7; --trusted-pwsh pins its absolute executable path.\nThe packaged Luna-fast worker maps to native gpt-6-luna with the priority (Fast) service tier.\nUses existing ChatGPT authentication and native Codex permissions.";
2
+ export declare const CODEX_MISSION_USAGE = "Usage: sortie-dogs codex mission --prompt <text>\n [--resume <root-thread-id>] [--project-root <path>] [--executable <codex>] [--model <model>] [--effort <effort>] [--permissions <native-profile>] [--trusted-pwsh <absolute-pwsh.exe>]\n\nRuns the existing Mission Operator, Coordinator, Worker and Reviewer through Codex.\n--model and --effort explicitly override all roles; otherwise packaged role defaults apply.\n--permissions explicitly selects an existing native profile; omission retains native defaults.\nWindows uses existing PowerShell 7; --trusted-pwsh pins its absolute executable path.\nThe packaged ordinary worker uses native gpt-6.1-terra/max, Standard (no Fast/priority).\nThe retained Luna-fast alias maps to native gpt-6-luna with the priority (Fast) service tier.\nUses existing ChatGPT authentication and native Codex permissions.";
3
3
  export declare function runCodexMissionCommand(argv: readonly string[]): Promise<number>;
4
4
  /** Bounded user-facing progress; do not dump control packets or hidden reasoning. */
5
5
  export declare function codexMissionProgress(event: CodexTurnEvent & {
@@ -6,7 +6,8 @@ Runs the existing Mission Operator, Coordinator, Worker and Reviewer through Cod
6
6
  --model and --effort explicitly override all roles; otherwise packaged role defaults apply.
7
7
  --permissions explicitly selects an existing native profile; omission retains native defaults.
8
8
  Windows uses existing PowerShell 7; --trusted-pwsh pins its absolute executable path.
9
- The packaged Luna-fast worker maps to native gpt-6-luna with the priority (Fast) service tier.
9
+ The packaged ordinary worker uses native gpt-6.1-terra/max, Standard (no Fast/priority).
10
+ The retained Luna-fast alias maps to native gpt-6-luna with the priority (Fast) service tier.
10
11
  Uses existing ChatGPT authentication and native Codex permissions.`;
11
12
  export async function runCodexMissionCommand(argv) {
12
13
  if (argv.length === 1 && argv[0] === "--help") {
@@ -1,3 +1,12 @@
1
+ export interface MissionAttachment {
2
+ index: number;
3
+ part_id?: string;
4
+ mime?: string;
5
+ filename?: string;
6
+ url: string;
7
+ path?: string;
8
+ sha256?: string;
9
+ }
1
10
  export interface PublicMissionContext {
2
11
  id: string;
3
12
  role: "user" | "assistant";
@@ -12,6 +21,7 @@ export interface PublicMissionContext {
12
21
  exit?: number;
13
22
  time?: unknown;
14
23
  }[];
24
+ attachments?: MissionAttachment[];
15
25
  }
16
26
  export interface MissionContextSource {
17
27
  session_id: string;
@@ -20,6 +30,9 @@ export interface MissionContextSource {
20
30
  entries: number;
21
31
  observed_at: string;
22
32
  history_available: boolean;
33
+ attachments?: (MissionAttachment & {
34
+ message_id: string;
35
+ })[];
23
36
  }
24
37
  export interface MissionContinuity {
25
38
  sources: MissionContextSource[];
@@ -70,4 +83,7 @@ export declare function missionContextView(source: MissionContextSource, entries
70
83
  entries: number;
71
84
  observed_at: string;
72
85
  history_available: boolean;
86
+ attachments?: (MissionAttachment & {
87
+ message_id: string;
88
+ })[];
73
89
  };
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
5
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5
6
  /** Public task data only. Keep exact text/results in a lazy snapshot, not private reasoning. */
6
7
  export function publicMissionContext(messages) {
@@ -11,6 +12,11 @@ export function publicMissionContext(messages) {
11
12
  const parts = message.parts.filter(record);
12
13
  const text = parts.filter(part => part.type === "text" && part.synthetic !== true && typeof part.text === "string")
13
14
  .map(part => part.text).join("\n");
15
+ const attachments = info.role === "user" ? parts.filter(part => part.type === "file" && typeof part.url === "string")
16
+ .map((part, index) => ({ index, url: part.url,
17
+ ...(typeof part.id === "string" ? { part_id: part.id } : {}),
18
+ ...(typeof part.mime === "string" ? { mime: part.mime } : {}),
19
+ ...(typeof part.filename === "string" ? { filename: part.filename } : {}) })) : [];
14
20
  const tools = parts.flatMap(part => {
15
21
  if (part.type !== "tool" || typeof part.tool !== "string" || !record(part.state))
16
22
  return [];
@@ -23,17 +29,47 @@ export function publicMissionContext(messages) {
23
29
  ...(typeof metadata.exit === "number" ? { exit: metadata.exit } : {}),
24
30
  ...(part.time !== undefined ? { time: part.time } : {}) }];
25
31
  });
26
- return text.trim() || tools.length ? [{ id: info.id, role: info.role, text, tools }] : [];
32
+ return text.trim() || tools.length || attachments.length ? [{ id: info.id, role: info.role, text, tools,
33
+ ...(attachments.length ? { attachments } : {}) }] : [];
27
34
  });
28
35
  }
36
+ /** Store inline bytes once, not megabytes of base64 in every prompt/contract. No network fetch or new permission. */
37
+ async function retainAttachment(directory, attachment) {
38
+ if (!attachment.url.startsWith("data:")) {
39
+ try {
40
+ return attachment.url.startsWith("file:") ? { ...attachment, path: fileURLToPath(attachment.url) } : attachment;
41
+ }
42
+ catch {
43
+ return attachment;
44
+ } // Keep a host-specific URI usable on its owning host, not a new admission gate.
45
+ }
46
+ const comma = attachment.url.indexOf(",");
47
+ if (comma < 0)
48
+ return attachment;
49
+ const header = attachment.url.slice(0, comma), data = attachment.url.slice(comma + 1);
50
+ const bytes = /;base64$/i.test(header) ? Buffer.from(data, "base64") : Buffer.from(decodeURIComponent(data));
51
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
52
+ const extension = { "image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp",
53
+ "application/pdf": ".pdf", "text/plain": ".txt" }[attachment.mime ?? ""] ?? ".bin";
54
+ const path = join(directory, "attachments", `${sha256}${extension}`);
55
+ await mkdir(join(directory, "attachments"), { recursive: true });
56
+ await writeFile(path, bytes, { flag: "wx" }).catch(error => {
57
+ if (error.code !== "EEXIST")
58
+ throw error;
59
+ });
60
+ return { ...attachment, url: pathToFileURL(path).href, path, sha256 };
61
+ }
29
62
  /** Preserve earlier available entries when the host later returns a compacted/partial history. */
30
63
  export async function retainMissionContext(directory, sessionID, messages, previous) {
31
64
  const current = publicMissionContext(messages);
32
65
  const retained = previous ? await readFile(previous.path, "utf8")
33
66
  .then(content => JSON.parse(content).entries).catch(() => undefined) : undefined;
34
67
  const merged = new Map((retained ?? []).map(entry => [entry.id, entry]));
35
- for (const entry of current)
36
- merged.set(entry.id, entry);
68
+ for (const entry of current) {
69
+ const attachments = entry.attachments ? await Promise.all(entry.attachments.map(file => retainAttachment(directory, file)))
70
+ : merged.get(entry.id)?.attachments;
71
+ merged.set(entry.id, { ...entry, ...(attachments?.length ? { attachments } : {}) });
72
+ }
37
73
  const entries = [...merged.values()];
38
74
  const content = JSON.stringify({ session_id: sessionID, entries }, null, 2) + "\n";
39
75
  const sha256 = createHash("sha256").update(content).digest("hex");
@@ -42,8 +78,10 @@ export async function retainMissionContext(directory, sessionID, messages, previ
42
78
  await mkdir(directory, { recursive: true });
43
79
  await writeFile(path, content);
44
80
  }
81
+ const attachments = entries.flatMap(entry => (entry.attachments ?? []).map(file => ({ message_id: entry.id, ...file })));
45
82
  return { source: { session_id: sessionID, path, sha256, entries: entries.length,
46
- observed_at: new Date().toISOString(), history_available: current.length > 0 }, entries };
83
+ observed_at: new Date().toISOString(), history_available: current.length > 0,
84
+ ...(attachments.length ? { attachments } : {}) }, entries };
47
85
  }
48
86
  /** Small inline view, with explicit pointers to every omitted byte in the exact snapshot. */
49
87
  export function missionContextView(source, entries, suppliedRequestIDs = []) {
@@ -338,7 +338,7 @@ export class OperatorMissionRuntime {
338
338
  corrections: (state.corrections ?? []).map(({ initialPrompt: _prompt, inlineReview: _inline, ...correction }) => correction),
339
339
  retained_review: state.review ? { run_id: state.review.runID, verdict: state.review.verdict,
340
340
  mode: state.review.mode, author: state.review.child, result: state.review.result, self_recheck: state.review.selfRecheck } : null,
341
- 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." } };
341
+ guidance: "Current original_requests/requirements govern; older plans remain historical. Original attachments are in sources[].attachments, ordered within each message; read their path/URL when relevant, not a prose substitute. Message IDs and indices preserve provenance, not an inferred mapping to user-assigned image numbers. 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." } };
342
342
  }
343
343
  async load(file) {
344
344
  try {
@@ -870,6 +870,7 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
870
870
  }
871
871
  })));
872
872
  return { original_requests: mission.requests, requirements: mission.requirements,
873
+ original_attachments: (mission.continuity?.sources ?? []).flatMap(source => (source.attachments ?? []).map(file => ({ session_id: source.session_id, ...file }))),
873
874
  operation: missionOperationSummary(mission),
874
875
  history: { status: missingAnchors.length ? "unavailable" : history.status,
875
876
  ...(history.reason ? { reason: history.reason } : missingAnchors.length ? { reason: "accepted-handoff-anchor-unavailable" } : {}),
@@ -887,7 +888,7 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
887
888
  observation: mission.deliveryObservation && mission.deliveryObservation.run_id === current?.runID ? mission.deliveryObservation : null,
888
889
  observation_source: "persisted operator Git lifecycle and formal validation records; no new Git inspection",
889
890
  clean: mission.deliveryObservation && mission.deliveryObservation.run_id === current?.runID ? mission.deliveryObservation.clean : "not independently observed by this projection" },
890
- 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." };
891
+ interpretation: "Compare original requests (including relevant original attachments) with the submitted candidate and what the actual evidence exercises. Functional PASS, Review PASS, or an honestly recorded unverified limitation does not establish a required visual comparison. If a mandatory outcome remains unmet, continue authorized retrieval, inspection or correction without waiting for the user. Optional caveats, valid alternative checks and reusable evidence need not block acceptance; do not reject on the word unverified alone or repeat all checks yourself. No new approval, proof file, extra Reviewer, universal screenshots or pixel-exact matching is required. Historical PASS is not current PASS. Inspect concrete gaps, not routine archive searches or full source rereads. Existing completion and Review guards still apply." };
891
892
  }
892
893
  export function missionReviewIndependent(mission, child) {
893
894
  return child !== undefined && !mission.sourceAuthors?.includes(child) &&
@@ -8488,12 +8488,16 @@ export const SortieDogsPlugin = async (input, options) => {
8488
8488
  return settled;
8489
8489
  },
8490
8490
  releaseDirectUnit: async (actor) => {
8491
- await releaseWriteGate(actor);
8491
+ const released = JSON.parse(await releaseWriteGate(actor));
8492
+ if (released.status === "denied")
8493
+ throw new Error(`mission-direct-release:${released.reason}`);
8492
8494
  releaseSessionEnforcement(actor);
8493
8495
  },
8494
8496
  assertDirectUnitQuiescent: async (actor) => {
8495
8497
  if (activeSessions.get(actor)?.inFlightCalls.size)
8496
8498
  throw new Error("mission-direct-tools-running");
8499
+ if (bindingOperations.has(actor))
8500
+ throw new Error("mission-direct-binding-in-flight");
8497
8501
  },
8498
8502
  failDirectUnit: async (root, actor) => {
8499
8503
  const direct = await input.runtimeBridge?.directMissionExecution?.(actor);
@@ -1,9 +1,9 @@
1
1
  export declare const MODEL_COST_PRICING_SNAPSHOT: {
2
- readonly checkedAt: "2026-09-30";
2
+ readonly checkedAt: "2026-10-27";
3
3
  readonly currency: "USD";
4
4
  readonly unit: "1M tokens";
5
- readonly sources: readonly ["https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/models/gpt-6-astra", "https://developers.openai.com/api/docs/models/gpt-6-sol", "https://developers.openai.com/api/docs/models/gpt-6.1-sol", "https://developers.openai.com/api/docs/models/gpt-6-luna", "https://developers.openai.com/api/docs/guides/fast-mode", "https://developers.openai.com/api/docs/models/gpt-5.6-sol", "https://developers.openai.com/api/docs/models/gpt-5.6-terra", "https://developers.openai.com/api/docs/models/gpt-5.6-luna", "https://platform.claude.com/docs/en/about-claude/pricing"];
6
- readonly assumptions: readonly ["OpenAI Standard pricing; requests above 272,000 total input/cache tokens use 2x input/cache and 1.5x output pricing.", "Anthropic Standard pricing with the 5-minute cache-write rate; Batch, Flex, regional, tool, and subscription charges are excluded.", "variant and serviceTier are separate; model variants do not alter the selected Standard token price.", "GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Luna use their official Standard schedules; the OpenCode gpt-6-luna-fast selector sends API model gpt-6-luna with priority service tier.", "For GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Luna, Fast mode (service_tier fast or priority, including the gpt-6.1-sol-fast selector) multiplies applicable Standard rates by 2, including cache and long-context multipliers.", "gpt-5.6-luna-fast is retained only as a compatibility price for historical host catalog entries; it has no published model page and is exactly twice the GPT-5.6 Luna Standard schedule."];
5
+ readonly sources: readonly ["https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/models/gpt-6-astra", "https://developers.openai.com/api/docs/models/gpt-6-sol", "https://developers.openai.com/api/docs/models/gpt-6.1-sol", "https://developers.openai.com/api/docs/models/gpt-6.1-terra", "https://developers.openai.com/api/docs/models/gpt-6-luna", "https://developers.openai.com/api/docs/guides/fast-mode", "https://developers.openai.com/api/docs/models/gpt-5.6-sol", "https://developers.openai.com/api/docs/models/gpt-5.6-terra", "https://developers.openai.com/api/docs/models/gpt-5.6-luna", "https://platform.claude.com/docs/en/about-claude/pricing"];
6
+ readonly assumptions: readonly ["OpenAI Standard pricing; requests above 272,000 total input/cache tokens use 2x input/cache and 1.5x output pricing.", "Anthropic Standard pricing with the 5-minute cache-write rate; Batch, Flex, regional, tool, and subscription charges are excluded.", "variant and serviceTier are separate; model variants do not alter the selected Standard token price.", "GPT-6.1 Terra Standard uses $1 input, $0.05 cache read, $1.25 30-minute cache write and $5 output per 1M tokens; subscription usage/limits are separate from this API-equivalent estimate.", "GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Luna use their official Standard schedules; the OpenCode gpt-6-luna-fast selector sends API model gpt-6-luna with priority service tier.", "For GPT-6 Sol, GPT-6.1 Sol, GPT-6.1 Terra, and GPT-6 Luna, Fast mode (service_tier fast or priority, including the gpt-6.1-sol-fast selector) multiplies applicable Standard rates by 2, including cache and long-context multipliers.", "gpt-5.6-luna-fast is retained only as a compatibility price for historical host catalog entries; it has no published model page and is exactly twice the GPT-5.6 Luna Standard schedule."];
7
7
  };
8
8
  export interface ModelCostUsage {
9
9
  readonly providerID: string | undefined;
@@ -1,5 +1,5 @@
1
1
  export const MODEL_COST_PRICING_SNAPSHOT = {
2
- checkedAt: "2026-09-30",
2
+ checkedAt: "2026-10-27",
3
3
  currency: "USD",
4
4
  unit: "1M tokens",
5
5
  sources: [
@@ -7,6 +7,7 @@ export const MODEL_COST_PRICING_SNAPSHOT = {
7
7
  "https://developers.openai.com/api/docs/models/gpt-6-astra",
8
8
  "https://developers.openai.com/api/docs/models/gpt-6-sol",
9
9
  "https://developers.openai.com/api/docs/models/gpt-6.1-sol",
10
+ "https://developers.openai.com/api/docs/models/gpt-6.1-terra",
10
11
  "https://developers.openai.com/api/docs/models/gpt-6-luna",
11
12
  "https://developers.openai.com/api/docs/guides/fast-mode",
12
13
  "https://developers.openai.com/api/docs/models/gpt-5.6-sol",
@@ -18,8 +19,9 @@ export const MODEL_COST_PRICING_SNAPSHOT = {
18
19
  "OpenAI Standard pricing; requests above 272,000 total input/cache tokens use 2x input/cache and 1.5x output pricing.",
19
20
  "Anthropic Standard pricing with the 5-minute cache-write rate; Batch, Flex, regional, tool, and subscription charges are excluded.",
20
21
  "variant and serviceTier are separate; model variants do not alter the selected Standard token price.",
22
+ "GPT-6.1 Terra Standard uses $1 input, $0.05 cache read, $1.25 30-minute cache write and $5 output per 1M tokens; subscription usage/limits are separate from this API-equivalent estimate.",
21
23
  "GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Luna use their official Standard schedules; the OpenCode gpt-6-luna-fast selector sends API model gpt-6-luna with priority service tier.",
22
- "For GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Luna, Fast mode (service_tier fast or priority, including the gpt-6.1-sol-fast selector) multiplies applicable Standard rates by 2, including cache and long-context multipliers.",
24
+ "For GPT-6 Sol, GPT-6.1 Sol, GPT-6.1 Terra, and GPT-6 Luna, Fast mode (service_tier fast or priority, including the gpt-6.1-sol-fast selector) multiplies applicable Standard rates by 2, including cache and long-context multipliers.",
23
25
  "gpt-5.6-luna-fast is retained only as a compatibility price for historical host catalog entries; it has no published model page and is exactly twice the GPT-5.6 Luna Standard schedule.",
24
26
  ],
25
27
  };
@@ -27,6 +29,7 @@ const OPENAI = {
27
29
  "gpt-6-astra": { input: 10, cacheRead: 1, cacheWrite: 12.5, output: 50 },
28
30
  "gpt-6-sol": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 10 },
29
31
  "gpt-6.1-sol": { input: 2, cacheRead: 0.1, cacheWrite: 2.5, output: 10 },
32
+ "gpt-6.1-terra": { input: 1, cacheRead: 0.05, cacheWrite: 1.25, output: 5 },
30
33
  "gpt-6-luna": { input: 0.1, cacheRead: 0.01, cacheWrite: 0.125, output: 0.5 },
31
34
  "gpt-5.6-sol": { input: 4, cacheRead: 0.4, cacheWrite: 5, output: 20 },
32
35
  "gpt-5.6": { input: 4, cacheRead: 0.4, cacheWrite: 5, output: 20 },
@@ -49,7 +52,7 @@ export function estimateModelUsageCost(usage) {
49
52
  const sol61FastAlias = usage.providerID?.toLowerCase() === "openai" && usage.modelID === "gpt-6.1-sol-fast";
50
53
  const alias = lunaFastAlias || sol61FastAlias;
51
54
  const fast = alias || (["fast", "priority"].includes(usage.serviceTier?.toLowerCase() ?? "") &&
52
- usage.providerID?.toLowerCase() === "openai" && ["gpt-6-sol", "gpt-6.1-sol", "gpt-6-luna"].includes(usage.modelID ?? ""));
55
+ usage.providerID?.toLowerCase() === "openai" && ["gpt-6-sol", "gpt-6.1-sol", "gpt-6.1-terra", "gpt-6-luna"].includes(usage.modelID ?? ""));
53
56
  if (usage.serviceTier !== undefined && !(fast ? ["standard", "default", "fast", "priority"] : ["standard", "default"]).includes(usage.serviceTier.toLowerCase())) {
54
57
  return { status: "unpriced", reason: "unsupported-service-tier" };
55
58
  }
@@ -36,7 +36,8 @@ const SERIAL_OPTIONAL_ARGUMENTS = new Map([
36
36
  "scope", "trigger", "cause", "prevention", "evidence", "evidenceRef", "id", "promotedRef", "confirmation",
37
37
  ])],
38
38
  ]);
39
- const PREVIEW_WORKER_ROUTE = Object.freeze({ model: "openai/gpt-6-luna-fast", variant: "max" });
39
+ const PREVIEW_WORKER_ROUTE = Object.freeze({ model: "openai/gpt-6.1-terra", variant: "max" });
40
+ const PREVIEW_LUNA_WORKER_ROUTE = Object.freeze({ model: "openai/gpt-6-luna-fast", variant: "max" });
40
41
  const PREVIEW_SCOUT_ROUTE = Object.freeze({ model: "openai/gpt-6-luna-fast", variant: "max" });
41
42
  /**
42
43
  * Contract authorship, not throughput. Qualification observed a cheaper operations model emit
@@ -49,7 +50,7 @@ const PREVIEW_PRIMARY_ROUTE = Object.freeze({ model: "openai/gpt-6.1-sol", varia
49
50
  const PREVIEW_REVIEW_ROUTE = Object.freeze({ model: "openai/gpt-6.1-sol", variant: "xhigh" });
50
51
  /** Every preview route the profile can bind a role to. Catalog declaration reads this one list. */
51
52
  const PREVIEW_ROUTES = Object.freeze([
52
- PREVIEW_PRIMARY_ROUTE, PREVIEW_WORKER_ROUTE, PREVIEW_SCOUT_ROUTE, PREVIEW_OPERATIONS_ROUTE,
53
+ PREVIEW_PRIMARY_ROUTE, PREVIEW_WORKER_ROUTE, PREVIEW_LUNA_WORKER_ROUTE, PREVIEW_SCOUT_ROUTE, PREVIEW_OPERATIONS_ROUTE,
53
54
  PREVIEW_REVIEW_ROUTE,
54
55
  ]);
55
56
  function missionReportReview(mission, runID) {
@@ -1492,8 +1493,8 @@ export function createProfiledPlugin(profile, assetVersion) {
1492
1493
  }
1493
1494
  async function stop(root, reason, retireRoot = true) {
1494
1495
  const directActor = (await operators.read(root))?.units.find(unit => unit.status === "running" && unit.directExecution)?.directExecution?.actor;
1495
- if (directActor)
1496
- await activateDirect(directActor);
1496
+ // Stopping owned execution does not write its artifacts. Its existing reservation
1497
+ // is reconciled by cancelChildren, even when activation never acquired a writer.
1497
1498
  if (retireRoot)
1498
1499
  retired.add(root);
1499
1500
  if (retireRoot)
@@ -2289,7 +2290,9 @@ export function createProfiledPlugin(profile, assetVersion) {
2289
2290
  record(part.state.metadata) && ["running", "pending", "unknown"].includes(String(part.state.metadata.status)))) {
2290
2291
  return JSON.stringify({ status: "direct-handoff-awaits-tools", next_action: "Finish the existing native command before handing off this unit; retain its result and continue here or delegate afterwards." });
2291
2292
  }
2292
- await activateDirect(actor);
2293
+ // Yielding execution is management, not an artifact write. In particular, a
2294
+ // direct admission can exist even when its writer could not bind because another
2295
+ // live owner overlaps. Do not require that contested binding to hand it off.
2293
2296
  const mission = await missions.retainContext(root, actor, history, notes);
2294
2297
  await operators.handoffDirect(root, actor, await missions.handoffContext(mission));
2295
2298
  }
@@ -3599,7 +3602,7 @@ export function createProfiledPlugin(profile, assetVersion) {
3599
3602
  else if (args.status === "blocked" || args.status === "needs-decision") {
3600
3603
  // This owner is stopping its direct work, not validating it. Keep the
3601
3604
  // failed checks/candidate and settle its existing reservation once.
3602
- const direct = await activateDirect(context.sessionID);
3605
+ const direct = await directExecution(context.sessionID);
3603
3606
  if (direct)
3604
3607
  await control.failDirectUnit(root, context.sessionID);
3605
3608
  }
@@ -3811,7 +3814,7 @@ export function createProfiledPlugin(profile, assetVersion) {
3811
3814
  const agent = record(agents[name]) ? agents[name] : undefined;
3812
3815
  if (!agent)
3813
3816
  continue;
3814
- const route = role === "dog-scout" ? PREVIEW_SCOUT_ROUTE : PREVIEW_WORKER_ROUTE;
3817
+ const route = role === "dog-scout" ? PREVIEW_SCOUT_ROUTE : PREVIEW_LUNA_WORKER_ROUTE;
3815
3818
  agent.model ??= route.model;
3816
3819
  if (agent.variant === undefined && agent.model === route.model)
3817
3820
  agent.variant = route.variant;
@@ -4014,7 +4017,11 @@ export function createProfiledPlugin(profile, assetVersion) {
4014
4017
  throw new Error(RUNTIME_PROFILE_SESSION_INACTIVE);
4015
4018
  if (request.tool.startsWith("sortie_") && !request.tool.startsWith(profile.toolPrefix))
4016
4019
  throw new Error("runtime-profile-tool-mismatch");
4017
- const direct = await activateDirect(request.sessionID);
4020
+ // Management can reconcile, yield, stop or report the owner's admission.
4021
+ // They must remain reachable when its artifact binding is the thing in conflict.
4022
+ // Ordinary tools and the parts of planning that actually write still activate it.
4023
+ const direct = request.tool === status || request.tool === planUnits || request.tool === cancel || request.tool === submitMission
4024
+ ? await directExecution(request.sessionID) : await activateDirect(request.sessionID);
4018
4025
  const writer = who.role === "dog-worker" || !!direct || !!await reviewerCorrection(request.sessionID);
4019
4026
  if (who.role === "dog-reviewer" && writer) {
4020
4027
  const correction = (await reviewerCorrection(request.sessionID));
package/dist/plugin/v2.js CHANGED
@@ -90,6 +90,22 @@ function legacyPart(value, session, message) {
90
90
  ...(time ? { time: { start: time.ran ?? time.created, end: time.completed } } : {}),
91
91
  ...(output === undefined ? {} : { output }) } };
92
92
  }
93
+ /** Resolved V2 files carry the original bytes, even when their source URI is no longer readable. */
94
+ function legacyUserParts(text, files, session, message) {
95
+ return [{ id: `${message}-text`, sessionID: session, messageID: message, type: "text", text },
96
+ ...array(files).flatMap((file, index) => {
97
+ if (!record(file))
98
+ return [];
99
+ const mime = string(file.mime) ?? "application/octet-stream";
100
+ const url = typeof file.data === "string" ? `data:${mime};base64,${file.data}`
101
+ : string(file.uri); // The prompt hook runs before URI resolution.
102
+ if (!url)
103
+ return [];
104
+ return [{ id: `${message}-file-${index}`, sessionID: session, messageID: message, type: "file", mime, url,
105
+ ...(typeof file.name === "string" ? { filename: file.name } : {}),
106
+ ...(record(file.source) ? { source: file.source } : {}) }];
107
+ })];
108
+ }
93
109
  function legacyMessage(value, session, agent) {
94
110
  if (!record(value) || typeof value.id !== "string" || typeof value.type !== "string")
95
111
  return undefined;
@@ -97,7 +113,7 @@ function legacyMessage(value, session, agent) {
97
113
  const originalAgent = record(value.metadata) ? string(value.metadata.agent) ?? agent : agent;
98
114
  if (value.type === "user") {
99
115
  return { info: { id, sessionID: session, role: "user", agent: originalAgent, time: value.time },
100
- parts: [{ id: `${id}-text`, sessionID: session, messageID: id, type: "text", text: String(value.text ?? "") }] };
116
+ parts: legacyUserParts(String(value.text ?? ""), value.files, session, id) };
101
117
  }
102
118
  if (value.type !== "assistant") {
103
119
  if (value.type === "synthetic")
@@ -289,8 +305,22 @@ function toolContentText(content) {
289
305
  return values.length === 0 ? undefined : values.join("\n");
290
306
  }
291
307
  function replaceToolContent(result, text) {
292
- return typeof result.content === "string" ? { ...result, content: text }
293
- : { ...result, content: [{ type: "text", text }] };
308
+ if (toolContentText(result.content) === text)
309
+ return result;
310
+ if (typeof result.content === "string")
311
+ return { ...result, content: text };
312
+ let replaced = false;
313
+ const content = array(result.content).flatMap(item => {
314
+ if (!record(item) || item.type !== "text" || typeof item.text !== "string")
315
+ return [item];
316
+ if (replaced)
317
+ return [];
318
+ replaced = true;
319
+ return [{ ...item, text }];
320
+ });
321
+ // Only the legacy textual projection changed. Native file/image blocks must still reach
322
+ // the host's model-input lowering, including images returned through Code Mode.
323
+ return { ...result, content: replaced ? content : [...content, { type: "text", text }] };
294
324
  }
295
325
  function assistantText(message) {
296
326
  if (!record(message) || message.type !== "assistant" || message.finish !== "stop" || typeof message.id !== "string")
@@ -542,7 +572,7 @@ async function registerV2Hooks(context, hooks, background, liveState) {
542
572
  ["dog-reviewer-v010", "gpt-6.1-sol", "xhigh"],
543
573
  ["dog-scout-v010", "gpt-6-luna-fast", "max"],
544
574
  ["dog-luna-worker-v010", "gpt-6-luna-fast", "max"],
545
- ["dog-worker-v010", "gpt-6-luna-fast", "max"],
575
+ ["dog-worker-v010", "gpt-6.1-terra", "max"],
546
576
  ])
547
577
  editor.update(name, agent => { agent.model ??= { providerID: "openai", id, variant }; });
548
578
  editor.update("dog-reviewer-v010", agent => {
@@ -784,7 +814,7 @@ async function registerV2Hooks(context, hooks, background, liveState) {
784
814
  selectedChildModels.add(String(event.sessionID));
785
815
  }
786
816
  const output = { message: { id: String(event.messageID ?? ""), agent: string(info.agent), model },
787
- parts: [{ type: "text", text: legacyText }] };
817
+ parts: legacyUserParts(legacyText, event.prompt.files, String(event.sessionID), String(event.messageID ?? "")) };
788
818
  await hooks["chat.message"]({ sessionID: String(event.sessionID), messageID: String(event.messageID ?? ""),
789
819
  ...(typeof info.agent === "string" ? { agent: info.agent } : {}), model }, output);
790
820
  await reconcileCorrectionPermissions(String(event.sessionID));
@@ -427,7 +427,8 @@ export const runtimeAssets = Object.freeze([
427
427
  content = content.replace("mode: subagent\n", "mode: subagent\nhidden: true\n");
428
428
  }
429
429
  if (["dog-worker", "dog-luna-worker", "dog-scout"].includes(asset.name)) {
430
- content = content.replace("mode: subagent\n", "mode: subagent\nmodel: openai/gpt-6-luna-fast#max\n");
430
+ const model = asset.name === "dog-worker" ? "openai/gpt-6.1-terra#max" : "openai/gpt-6-luna-fast#max";
431
+ content = content.replace("mode: subagent\n", `mode: subagent\nmodel: ${model}\n`);
431
432
  }
432
433
  if (["dog-reviewer", "dog-advisor"].includes(asset.name)) {
433
434
  content = content.replace("mode: subagent\n", "mode: subagent\nmodel: openai/gpt-6.1-sol#xhigh\n");
@@ -155,6 +155,17 @@ ${MISSION_DEPENDENCY_WORKFLOW}
155
155
  SAME mission before any Worker starts, retains its Coordinator and cumulative spend, and checks
156
156
  old children before preparing a Worker. This is not permission to discard unchanged acceptance:
157
157
  if the scope is uncertain, ask the user which requirements remain instead of inferring a replacement.
158
+ When execution management is inconsistent or manifest-overlap blocks the owning direct writer,
159
+ explain the observed stop, what can be reconciled, and that the existing requirements, results,
160
+ checks and cumulative spend remain. Offer to recover and continue in ordinary conversation;
161
+ an ordinary affirmative reply or a request to self-recover is enough. No hidden command or ID is
162
+ needed from the user, and ordinary autonomous recovery does not require a new approval.
163
+ Inspect operator_status to reconcile only native-proven ended work. If this session owns a quiescent
164
+ direct admission that cannot bind, plan_units with executor: "worker" and no units yields that same
165
+ unit and reservation without acquiring the contested writer. Explain the result and continue the
166
+ returned Task when its write scope is available. A genuinely live or unproven other owner is not
167
+ released: wait or separate work, and do not dispatch repeated conflicting attempts. Do not invent
168
+ terminal state, validation success, a fresh Mission, or a budget reset.
158
169
  If a Coordinator reports mission-replan-worker-still-active after an interrupted native Task,
159
170
  it cannot call cancel_operator (root-only). Inspect operator_status and the native Task outcome.
160
171
  If the Worker is still active, wait, not duplicate. If the user chose to stop/replan, root calls
@@ -216,7 +227,17 @@ ${MISSION_DEPENDENCY_WORKFLOW}
216
227
  and cumulative budget. Use its Coordinator Task only for real coordination, contract discovery or
217
228
  correction that cannot be handled directly. Never replace an active Worker or ask for routine approval.
218
229
  4. After the review decision (including a justified low-risk skip), compare the completion candidate
219
- against the original request, real source and observed evidence before final acceptance.
230
+ against the original request, real source and observed evidence before final acceptance.
231
+ Include relevant original attachments: continuity sources retain ordered, message-linked read references
232
+ to the original bytes, not a descriptive substitute. Retrieve/inspect those references when needed.
233
+ Judge what each existing check actually exercises. Functional PASS, Review PASS, or honestly retaining
234
+ an unverified caveat does not establish a required visual comparison. If a mandatory outcome is unmet,
235
+ continue authorized retrieval, inspection or correction without waiting for a user reminder or approval.
236
+ Choose the effective method/author; reuse applicable evidence rather than repeat every check yourself.
237
+ Optional caveats and valid alternative verification can permit acceptance: the word unverified alone
238
+ is not a stop rule. This adds no approval gate, proof file, extra Reviewer, universal screenshot duty
239
+ or pixel-exact matching requirement. Keep autonomy, efficiency and visibility: report the actual gap
240
+ and next action, not an unsupported completion claim or a new restriction on ordinary work.
220
241
  For a Coordinator ready candidate, use operator_status's acceptance_summary: verbatim original
221
242
  requests, anchored cumulative formal validation, current review disposition (author self-recheck
222
243
  is not independent approval) and recorded delivery state.
@@ -484,6 +505,10 @@ in this SAME session when your existing context makes that efficient. You are no
484
505
  Use the Operator's preferred_executor as a starting recommendation. Start useful work with available context;
485
506
  do not investigate merely to confirm the recommendation. Adapt through actual work, honoring explicit user choices.
486
507
  Use plan_units with executor: "self" for direct work, or start_direct_unit for an already prepared unit.
508
+ If a direct admission cannot bind because of manifest-overlap, operator_status and plan_units with
509
+ executor: "worker" and no units remain management operations: yield only this quiescent owner, retain
510
+ its checks and reservation, and wait for a real conflicting writer rather than evicting it. Continue
511
+ the same Mission and actual remaining work; do not create another Mission to clear the conflict.
487
512
  The host binds your native check results to the candidate; finish_direct_unit closes that work without
488
513
  a Worker handoff. Independent Review and Operator final acceptance still follow. This is not limited to
489
514
  micro-edits. Delegate to the configured Worker when that reduces work/cost or provides useful separation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.14.5",
3
+ "version": "0.14.7",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",