sortie-dogs 0.13.9 → 0.13.11

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.
@@ -24,7 +24,7 @@ Check the selected entrypoint with `<entrypoint> codex --help` before starting w
24
24
 
25
25
  ## Choose one route
26
26
 
27
- On a POSIX host with `/bin/bash`, use the natural-language Mission route:
27
+ Use the natural-language Mission route on POSIX and Windows:
28
28
 
29
29
  ```text
30
30
  <entrypoint> codex mission --project-root <repository-root> --prompt <exact-user-request>
@@ -32,11 +32,17 @@ On a POSIX host with `/bin/bash`, use the natural-language Mission route:
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
- On Windows, use the manifest-bound route until the full Mission shell path supports Windows natively:
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.
36
+
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
+
39
+ The compatibility tool named `bash` executes PowerShell commands on Windows and POSIX bash on POSIX. Do not supply POSIX commands to the Windows shell. Shell and file commands still use the configured native permissions, or an explicitly integrated host executor. A sandbox startup failure is not a validation failure or success: preserve the failed execution and report the environment blocker; do not widen permissions or stop unrelated Codex processes.
40
+
41
+ Use the manifest-bound `codex run` route only for an explicitly selected single-task manifest workflow. It is not the full multi-role Mission route:
36
42
 
37
43
  1. Read repository instructions and inspect only enough to identify bounded project-relative read scope, write scope, and meaningful validation commands.
38
44
  2. Create one temporary manifest under `.sortie-dogs/codex-skill/` with `version`, `task_id`, `read`, `write`, and non-empty `validation`. Reject external or uncertain write scope instead of broadening it.
39
- 3. Resolve the exact PowerShell executable with `(Get-Command pwsh.exe -ErrorAction Stop).Source`. Invoke:
45
+ 3. On Windows the manifest workflow needs the exact PowerShell executable for command-envelope observation. Resolve it with `(Get-Command pwsh.exe -ErrorAction Stop).Source`, then invoke:
40
46
 
41
47
  ```text
42
48
  <entrypoint> codex run --project-root <repository-root> --manifest <temporary-manifest> --prompt <exact-user-request> --trusted-pwsh <resolved-pwsh.exe>
@@ -1 +1 @@
1
- 0.13.9-codex-skill-v1
1
+ 0.13.10-codex-skill-v2
package/README.md CHANGED
@@ -35,10 +35,10 @@ implementation, validation, review, and model routing.
35
35
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) ·
36
36
  [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md)
37
37
 
38
- **Current release: [v0.13.9](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.9)**
39
- ([release notes](docs/release-v0.13.9.md)). The default Mission runtime retains the `v010`
38
+ **Current release: [v0.13.10](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.10)**
39
+ ([release notes](docs/release-v0.13.10.md)). The default Mission runtime retains the `v010`
40
40
  profile, command and configuration names for compatibility; these names do not mean v0.10 is installed.
41
- The current asset marker is `0.13.9-codex-operation-v1`.
41
+ The current asset marker is `0.13.10-codex-windows-v1`.
42
42
 
43
43
  ## SWE-bench Lite: 170/300 (56.67%)
44
44
 
@@ -48,7 +48,7 @@ The fixed **Sortie-dogs v0.12.24** harness resolved **170 of 300 SWE-bench Lite
48
48
 
49
49
  The single official 300-instance report and frozen predictions are hash-bound in the report. Confirmed inference expense was **$162.99**; a separate **$34.60** of usage has unknown pricing and is held against the campaign cap, **not** counted as known expense. Leaderboard registration and maintainer acceptance are separate from this official local evaluation.
50
50
 
51
- Historical scores below belong to their fixed candidates, not v0.13.9. SWE-bench is a separate,
51
+ Historical scores below belong to their fixed candidates, not v0.13.10. SWE-bench is a separate,
52
52
  optional measurement rather than a mandatory release gate.
53
53
 
54
54
  > **Beta:** v0.13.x is still stabilizing. Runtime behavior,
@@ -63,11 +63,11 @@ Use Node.js 22.6 or newer and an existing Codex CLI with ChatGPT authentication.
63
63
  used Node.js 22.22.1 and Codex 0.160.1. Sortie neither installs Codex nor starts a login flow, copies
64
64
  credentials, or creates a second host configuration. Mission execution refuses non-ChatGPT auth.
65
65
 
66
- The Codex Mission adapter and explicit skill are included in v0.13.9. Install the release in the
66
+ The Codex Mission adapter and explicit skill are included in v0.13.10. Install the release in the
67
67
  target project and inspect the entrypoint before running a prompt:
68
68
 
69
69
  ```sh
70
- npm install --save-dev sortie-dogs@0.13.9
70
+ npm install --save-dev sortie-dogs@0.13.10
71
71
  npx --no-install sortie-dogs codex init .
72
72
  npx --no-install sortie-dogs codex mission --help
73
73
  ```
@@ -87,9 +87,10 @@ $sortie-dogs Implement and verify the requested change
87
87
  `$sortie-dogs` is the current direct Skills syntax. Codex does not expose arbitrary custom commands
88
88
  as exact `/sortie-dogs` slash commands; `/skills` opens the skill picker. The skill is intentionally
89
89
  explicit-only so child Codex Mission turns cannot invoke another Sortie Mission recursively.
90
- On POSIX it selects the natural-language Mission route below. On Windows it derives a bounded
91
- operation manifest and uses `codex run`, which retains the documented post-execution observation
92
- boundary instead of claiming a pre-execution guard.
90
+ On POSIX and Windows it selects the natural-language, multi-role Mission route below.
91
+ Windows uses existing PowerShell 7; it does not route natural-language Missions through the
92
+ single-task `codex run` manifest workflow. That workflow remains available when explicitly selected
93
+ and retains its post-execution observation boundary, not a pre-execution guard.
93
94
 
94
95
  Sortie-dogs also exports a host adapter for the official Codex app-server stdio protocol. This is
95
96
  separate from selecting an OpenAI model through OpenCode: OpenCode model routing still uses the
@@ -123,7 +124,7 @@ server-initiated requests fail closed.
123
124
  For the existing Operator → Coordinator → Worker → Reviewer Mission workflow, use:
124
125
 
125
126
  ```sh
126
- npx --no-install sortie-dogs codex mission --prompt "Implement and review the requested change" --model gpt-6.1-sol --effort medium
127
+ npx --no-install sortie-dogs codex mission --prompt "Implement and review the requested change"
127
128
  ```
128
129
 
129
130
  The equivalent public SDK entrypoint is:
@@ -133,8 +134,9 @@ import { CodexMissionSession } from "sortie-dogs";
133
134
 
134
135
  const mission = await CodexMissionSession.create({
135
136
  projectRoot: process.cwd(),
136
- model: "gpt-6.1-sol", // Choose a model available to the existing signed-in account.
137
- effort: "medium",
137
+ // Leave model/effort unset to preserve packaged role defaults.
138
+ // model: "gpt-6.1-sol", effort: "medium", // Explicit override for every role.
139
+ // trustedPowerShellExecutable: "C:/Program Files/PowerShell/7/pwsh.exe", // Windows, optional.
138
140
  // resumeThreadID: "saved-root-thread-id", // Continue the same Mission when needed.
139
141
  });
140
142
  try {
@@ -146,14 +148,26 @@ try {
146
148
  }
147
149
  ```
148
150
 
149
- This Linux-first route uses `CodexMissionSession` and the existing Mission tools, correction grants,
151
+ This route uses `CodexMissionSession` and the existing Mission tools, correction grants,
150
152
  validation evidence, and final Operator acceptance. It runs saved native Codex threads with existing
151
153
  ChatGPT authentication; it does not import OpenCode settings or introduce a second Mission ledger.
152
- Commands run through `/bin/bash` using the native app-server's configured permissions. Sortie does not
154
+ Commands run through `/bin/bash` on POSIX or existing `pwsh.exe -NoProfile -NonInteractive -Command`
155
+ on Windows, using the native app-server's configured permissions. The compatibility tool remains
156
+ named `bash`, but its Windows input must be PowerShell syntax. Windows callers can pin an absolute
157
+ PowerShell 7 path with SDK `trustedPowerShellExecutable` or CLI `--trusted-pwsh`.
158
+ Packaged roles retain Sol 6.1/xhigh for Operator, Coordinator and Reviewer. The shared Luna-fast/max
159
+ Worker alias maps only in Codex to native `gpt-6-luna`/max plus `serviceTier: "priority"` (Fast).
160
+ OpenCode routing is unchanged. Fast uses subscription limits faster than Standard; no metered API
161
+ fallback is introduced. CLI progress exposes each role's native model, effort and separate service tier.
162
+ Sortie does not
153
163
  replace them with a fixed repository-only or network-disabled policy, change host configuration, or select
154
164
  full access. Thread turns retain native thread permissions; standalone `command/exec` uses the server's
155
165
  configured policy, not a thread's temporary grants. A parent application's in-memory approval is not
156
166
  automatically transferred to a separately launched app-server.
167
+ Windows recovery can reclaim a killed adapter only when its recorded Windows PID is absent.
168
+ A live/reused PID, access denial, legacy owner without platform identity, or unresolved native
169
+ execution remains unknown; no command is replayed. Sandbox startup errors must be resolved at the
170
+ native host, not by silently widening Sortie's permissions or stopping unrelated Codex processes.
157
171
  SDK hosts can forward native command/file approval requests through `approval` and permission-subset
158
172
  requests through `permissionsApproval`. The native host remains responsible for deciding and enforcing
159
173
  the grant. The CLI reports approval requests but has no interactive approval bridge; without a connected
@@ -180,6 +194,11 @@ host must reconcile any still-running process. Omission keeps native `command/ex
180
194
  executor never falls back to native execution after rejection or failure. Existing Mission before/after
181
195
  hooks, command identity and validation acceptance remain shared. CLI progress identifies the selected
182
196
  executor; the CLI itself does not attach a parent executor.
197
+ File writes use a short argv referencing a temporary UTF-8 payload, so large files and literal quotes,
198
+ newlines or NUL characters do not overflow Windows command-line limits. Staging does not write the
199
+ project file; the same configured executor performs that write, and the payload is removed afterward.
200
+ Status observations (without `confirmed_conditions`) can return during an active child Task. Condition
201
+ registration and other modifying tools remain serialized; status does not imply Task completion.
183
202
  The bash tool accepts `workdir` (absolute or relative to the project root). The shared hooks and both
184
203
  executors use that directory; progress and receipts include the effective cwd. A successful command in
185
204
  another directory does not satisfy a validation declared for the project root.
@@ -289,7 +308,19 @@ existing local bridge loads enforcement and model routing. OpenCode can reload w
289
308
  but replacing an installed dependency may require a full restart. A new chat session alone does not
290
309
  prove the newly installed plugin is loaded.
291
310
 
292
- ## v0.13.9 runtime updates
311
+ ## v0.13.10 runtime updates
312
+
313
+ PR #169 connects natural-language Windows Codex Missions to the same multi-role lifecycle using
314
+ existing PowerShell 7, preserves command failure exits and native resume settings, and maps the
315
+ Luna-fast Worker alias to native Luna/max with the separate priority tier. Directory reads list
316
+ direct entries instead of requiring filename guesses. OpenCode model settings and native permission
317
+ profiles remain unchanged. PR #168 provides one host-aware Anko runner, reuses unchanged setup,
318
+ removes mandatory paid diagnostic probes, and preserves prior attempts, unknown usage and service
319
+ evidence. Its recorded v0.13.9 Anko completion and PR #169's Windows Codex host-executor completion
320
+ are historical fixed-candidate observations, not new v0.13.10 benchmark or native sandbox results.
321
+ See [release notes](docs/release-v0.13.10.md).
322
+
323
+ ## v0.13.9 runtime updates (retained)
293
324
 
294
325
  PRs #163–#166 connect native Codex sessions to the existing Mission lifecycle, add the explicit
295
326
  `$sortie-dogs` skill and `codex init`, restore operation Reviewer correction within existing write
@@ -654,7 +685,7 @@ register stable and `v010` from the same package installation path in one host.
654
685
  Project-local installation is recommended. To expose the current Mission assets globally:
655
686
 
656
687
  ```sh
657
- npm install --global sortie-dogs@0.13.9
688
+ npm install --global sortie-dogs@0.13.10
658
689
  sortie-dogs init --global --profile v010
659
690
  ```
660
691
 
@@ -666,7 +697,7 @@ can resolve a **separate dependency** under that config root. Updating npm-globa
666
697
  it. For that layout, also install the same release at the actual config root, then rerun global init:
667
698
 
668
699
  ```sh
669
- npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.9
700
+ npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.10
670
701
  sortie-dogs init --global --profile v010
671
702
  ```
672
703
 
@@ -689,7 +720,7 @@ version, preserves user configuration, and stops safely on unknown ownership or
689
720
  conflicting files.
690
721
 
691
722
  Align any exact version pin or separate bridge dependency with the intended release too. An installed
692
- marker of `0.13.9-codex-operation-v1` identifies the assets; it does not prove an already-running
723
+ marker of `0.13.10-codex-windows-v1` identifies the assets; it does not prove an already-running
693
724
  OpenCode process has reloaded the plugin.
694
725
 
695
726
  There is no supported uninstall command. Remove the npm dependency separately,
@@ -3,6 +3,6 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export declare const V010_RUNTIME_ASSET_VERSION = "0.13.9-codex-operation-v1";
7
- export declare const CODEX_SKILL_ASSET_VERSION = "0.13.9-codex-skill-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.13.11-operation-result-v1";
7
+ export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
8
8
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
@@ -3,5 +3,5 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export const V010_RUNTIME_ASSET_VERSION = "0.13.9-codex-operation-v1";
7
- export const CODEX_SKILL_ASSET_VERSION = "0.13.9-codex-skill-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.13.11-operation-result-v1";
7
+ export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
@@ -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>]\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.\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 Luna-fast worker 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 & {
@@ -1,10 +1,12 @@
1
1
  import { CodexMissionSession } from "../codex/mission-session.js";
2
2
  export const CODEX_MISSION_USAGE = `Usage: sortie-dogs codex mission --prompt <text>
3
- [--resume <root-thread-id>] [--project-root <path>] [--executable <codex>] [--model <model>] [--effort <effort>] [--permissions <native-profile>]
3
+ [--resume <root-thread-id>] [--project-root <path>] [--executable <codex>] [--model <model>] [--effort <effort>] [--permissions <native-profile>] [--trusted-pwsh <absolute-pwsh.exe>]
4
4
 
5
5
  Runs the existing Mission Operator, Coordinator, Worker and Reviewer through Codex.
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
+ 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.
8
10
  Uses existing ChatGPT authentication and native Codex permissions.`;
9
11
  export async function runCodexMissionCommand(argv) {
10
12
  if (argv.length === 1 && argv[0] === "--help") {
@@ -12,7 +14,7 @@ export async function runCodexMissionCommand(argv) {
12
14
  return 0;
13
15
  }
14
16
  const values = new Map();
15
- const allowed = new Set(["--prompt", "--resume", "--project-root", "--executable", "--model", "--effort", "--permissions"]);
17
+ const allowed = new Set(["--prompt", "--resume", "--project-root", "--executable", "--model", "--effort", "--permissions", "--trusted-pwsh"]);
16
18
  for (let index = 0; index < argv.length; index += 2) {
17
19
  const key = argv[index], value = argv[index + 1];
18
20
  if (!key || !allowed.has(key) || !value || value.startsWith("--") || values.has(key)) {
@@ -34,7 +36,8 @@ export async function runCodexMissionCommand(argv) {
34
36
  process.on("SIGINT", onInt);
35
37
  try {
36
38
  adapter = await CodexMissionSession.create({ projectRoot: values.get("--project-root") ?? process.cwd(),
37
- resumeThreadID: values.get("--resume"), permissions: values.get("--permissions"), executable: values.get("--executable"), model: values.get("--model"), effort: values.get("--effort"), onEvent: event => {
39
+ resumeThreadID: values.get("--resume"), permissions: values.get("--permissions"), executable: values.get("--executable"), model: values.get("--model"), effort: values.get("--effort"),
40
+ trustedPowerShellExecutable: values.get("--trusted-pwsh"), onEvent: event => {
38
41
  const progress = codexMissionProgress(event);
39
42
  if (progress)
40
43
  process.stderr.write(`${JSON.stringify(progress)}\n`);
@@ -63,6 +66,9 @@ export async function runCodexMissionCommand(argv) {
63
66
  }
64
67
  /** Bounded user-facing progress; do not dump control packets or hidden reasoning. */
65
68
  export function codexMissionProgress(event) {
69
+ if (event.method === "sortie/modelRoute")
70
+ return { thread_id: event.threadId, phase: "model-route", agent: event.params.agent, model: event.params.model,
71
+ effort: event.params.effort, service_tier: event.params.serviceTier };
66
72
  if (event.method === "sortie/permissions")
67
73
  return { thread_id: event.threadId, phase: "permissions", requested_profile: event.params.requestedProfile,
68
74
  effective_profile: event.params.profile, sandbox: event.params.sandbox, network_access: event.params.networkAccess,
@@ -34,6 +34,8 @@ export interface CodexTurnOptions {
34
34
  readonly approvalPolicy?: "untrusted" | "on-failure" | "on-request" | "never" | "unlessTrusted" | "onRequest";
35
35
  readonly model?: string;
36
36
  readonly effort?: string;
37
+ /** Native service tier, separate from the model slug (for example priority/Fast). */
38
+ readonly serviceTier?: string;
37
39
  readonly outputSchema?: JsonObject;
38
40
  readonly onEvent?: (event: CodexTurnEvent) => void | Promise<void>;
39
41
  }
@@ -90,6 +92,8 @@ export declare class CodexAppServerHost {
90
92
  private initialized;
91
93
  private readonly permissions;
92
94
  private readonly models;
95
+ private readonly serviceTiers;
96
+ private readonly efforts;
93
97
  private closed;
94
98
  private closing?;
95
99
  private fault?;
@@ -104,6 +108,7 @@ export declare class CodexAppServerHost {
104
108
  startThread(params: {
105
109
  cwd: string;
106
110
  model?: string;
111
+ serviceTier?: string;
107
112
  ephemeral?: boolean;
108
113
  permissions?: string;
109
114
  developerInstructions?: string;
@@ -111,6 +116,10 @@ export declare class CodexAppServerHost {
111
116
  config?: JsonObject;
112
117
  }): Promise<string>;
113
118
  threadModel(threadID: string): string | undefined;
119
+ /** Null means the native default was observed; undefined means no observation. */
120
+ threadServiceTier(threadID: string): string | null | undefined;
121
+ threadEffort(threadID: string): string | null | undefined;
122
+ private captureModelSettings;
114
123
  threadPermissions(threadID: string): JsonObject | undefined;
115
124
  private capturePermissions;
116
125
  /** Read only the authentication mode and plan class; never expose tokens or account identifiers. */
@@ -76,6 +76,8 @@ export class CodexAppServerHost {
76
76
  initialized = false;
77
77
  permissions = new Map();
78
78
  models = new Map();
79
+ serviceTiers = new Map();
80
+ efforts = new Map();
79
81
  closed = false;
80
82
  closing;
81
83
  fault;
@@ -93,7 +95,7 @@ export class CodexAppServerHost {
93
95
  async initialize() {
94
96
  if (this.initialized)
95
97
  return;
96
- await this.request("initialize", { clientInfo: { name: "sortie_dogs", title: "Sortie-dogs", version: this.options.clientVersion ?? "0.13.9" },
98
+ await this.request("initialize", { clientInfo: { name: "sortie_dogs", title: "Sortie-dogs", version: this.options.clientVersion ?? "0.13.10" },
97
99
  ...(this.options.dynamicTool || this.options.experimentalApi ? { capabilities: { experimentalApi: true } } : {}) });
98
100
  this.transport.send({ method: "initialized", params: {} });
99
101
  this.initialized = true;
@@ -102,6 +104,7 @@ export class CodexAppServerHost {
102
104
  await this.initialize();
103
105
  const result = await this.request("thread/start", { cwd: params.cwd, ...(params.model ? { model: params.model, allowProviderModelFallback: false } : {}),
104
106
  ...(params.ephemeral === undefined ? {} : { ephemeral: params.ephemeral }),
107
+ ...(params.serviceTier ? { serviceTier: params.serviceTier } : {}),
105
108
  ...(params.permissions ? { permissions: params.permissions } : {}),
106
109
  ...(params.developerInstructions ? { developerInstructions: params.developerInstructions } : {}),
107
110
  ...(params.dynamicTools ? { dynamicTools: params.dynamicTools } : {}),
@@ -109,12 +112,28 @@ export class CodexAppServerHost {
109
112
  const id = object(result) && object(result.thread) ? text(result.thread.id) : undefined;
110
113
  if (!id)
111
114
  throw new CodexHostError("protocol-invalid-response", "thread/start returned no thread id.");
112
- if (object(result) && typeof result.model === "string")
113
- this.models.set(id, result.model);
115
+ this.captureModelSettings(id, result);
114
116
  this.capturePermissions(id, result);
115
117
  return id;
116
118
  }
117
119
  threadModel(threadID) { return this.models.get(threadID); }
120
+ /** Null means the native default was observed; undefined means no observation. */
121
+ threadServiceTier(threadID) { return this.serviceTiers.get(threadID); }
122
+ threadEffort(threadID) { return this.efforts.get(threadID); }
123
+ captureModelSettings(threadID, result) {
124
+ if (!object(result))
125
+ return;
126
+ if (typeof result.model === "string")
127
+ this.models.set(threadID, result.model);
128
+ if (typeof result.serviceTier === "string")
129
+ this.serviceTiers.set(threadID, result.serviceTier);
130
+ else if (result.serviceTier === null)
131
+ this.serviceTiers.set(threadID, null);
132
+ if (typeof result.reasoningEffort === "string")
133
+ this.efforts.set(threadID, result.reasoningEffort);
134
+ else if (result.reasoningEffort === null)
135
+ this.efforts.set(threadID, null);
136
+ }
118
137
  threadPermissions(threadID) { return this.permissions.get(threadID); }
119
138
  capturePermissions(threadID, result) {
120
139
  if (!object(result))
@@ -149,6 +168,7 @@ export class CodexAppServerHost {
149
168
  const id = object(result) && object(result.thread) ? text(result.thread.id) : undefined;
150
169
  if (id !== threadID)
151
170
  throw new CodexHostError("protocol-invalid-response", "thread/resume returned a different thread id.");
171
+ this.captureModelSettings(id, result);
152
172
  this.capturePermissions(threadID, result);
153
173
  }
154
174
  async runTurn(threadID, prompt, options) {
@@ -160,6 +180,7 @@ export class CodexAppServerHost {
160
180
  ...(options.sandboxPolicy ? { sandboxPolicy: options.sandboxPolicy } : {}),
161
181
  ...(options.approvalPolicy ? { approvalPolicy: options.approvalPolicy } : {}),
162
182
  ...(options.model ? { model: options.model } : {}), ...(options.effort ? { effort: options.effort } : {}),
183
+ ...(options.serviceTier ? { serviceTier: options.serviceTier } : {}),
163
184
  ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}) });
164
185
  if (this.closed)
165
186
  throw new CodexHostError("server-closed", "Codex host was closed.");
@@ -34,6 +34,8 @@ export interface CodexMissionSessionOptions {
34
34
  }>>;
35
35
  readonly effort?: string;
36
36
  readonly executable?: string;
37
+ /** Windows only: existing absolute PowerShell 7 executable; otherwise resolve pwsh.exe on PATH. */
38
+ readonly trustedPowerShellExecutable?: string;
37
39
  readonly transportFactory?: () => CodexAppServerTransport;
38
40
  /** Delegate exact post-hook commands to the parent host; omission retains native command/exec. */
39
41
  readonly executeCommand?: CodexMissionCommandExecutor;
@@ -46,6 +48,7 @@ export interface CodexMissionSessionOptions {
46
48
  /** Native Codex transport for the existing Mission tools, roles and acceptance state. */
47
49
  export declare class CodexMissionSession {
48
50
  private readonly options;
51
+ private readonly shell;
49
52
  private hooks;
50
53
  private readonly sessions;
51
54
  private closed;
@@ -56,6 +59,7 @@ export declare class CodexMissionSession {
56
59
  private observations;
57
60
  private readonly ownedRoots;
58
61
  private readonly toolQueues;
62
+ private readonly observationsInFlight;
59
63
  readonly directory: string;
60
64
  private constructor();
61
65
  static create(options: CodexMissionSessionOptions): Promise<CodexMissionSession>;
@@ -8,10 +8,13 @@ import { V010_RUNTIME_PROFILE as profile } from "../core/runtime-profile.js";
8
8
  import { runtimeAssets } from "../runtime-assets-v010.js";
9
9
  import { OperatorRuntime } from "../core/operator-runtime.js";
10
10
  import { OperatorMissionRuntime, codexProcessOwner, codexOwnerGone } from "../core/operator-mission.js";
11
+ import { resolveCodexMissionShell } from "./mission-shell.js";
12
+ import { prepareCodexWrite } from "./mission-write.js";
11
13
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12
14
  /** Native Codex transport for the existing Mission tools, roles and acceptance state. */
13
15
  export class CodexMissionSession {
14
16
  options;
17
+ shell;
15
18
  hooks;
16
19
  sessions = new Map();
17
20
  closed = false;
@@ -22,13 +25,15 @@ export class CodexMissionSession {
22
25
  observations = [];
23
26
  ownedRoots = new Set();
24
27
  toolQueues = new Map();
28
+ observationsInFlight = new Set();
25
29
  directory;
26
- constructor(options) {
30
+ constructor(options, shell) {
27
31
  this.options = options;
32
+ this.shell = shell;
28
33
  this.directory = resolve(options.projectRoot);
29
34
  }
30
35
  static async create(options) {
31
- const adapter = new CodexMissionSession(options);
36
+ const adapter = new CodexMissionSession(options, await resolveCodexMissionShell(options.trustedPowerShellExecutable));
32
37
  adapter.owner = await codexProcessOwner();
33
38
  const id = (request) => request.path.id;
34
39
  const info = (session) => ({ id: session.id, agent: session.agent, parentID: session.parentID,
@@ -77,7 +82,7 @@ export class CodexMissionSession {
77
82
  }
78
83
  async close() {
79
84
  await this.stopHosts();
80
- await Promise.allSettled([...this.toolQueues.values()]);
85
+ await Promise.allSettled([...this.toolQueues.values(), ...this.observationsInFlight]);
81
86
  const missions = new OperatorMissionRuntime(this.directory, profile);
82
87
  for (const root of this.ownedRoots)
83
88
  await missions.codexRecovery(root, async (state) => {
@@ -266,6 +271,15 @@ export class CodexMissionSession {
266
271
  return await this.options.permissionsApproval?.(request) ?? { permissions: {}, scope: "turn" };
267
272
  },
268
273
  dynamicTool: call => {
274
+ // Observations must remain available while a Task awaits its child. Status
275
+ // with confirmed_conditions writes launch conditions and stays serialized.
276
+ if (call.tool === `${profile.toolPrefix}operator_status` && object(call.arguments) &&
277
+ legacyToolArgs(call.arguments, this.hooks.tool?.[call.tool]?.args ?? {}).confirmed_conditions === undefined) {
278
+ const observation = this.execute(call);
279
+ this.observationsInFlight.add(observation);
280
+ void observation.finally(() => this.observationsInFlight.delete(observation)).catch(() => undefined);
281
+ return observation;
282
+ }
269
283
  const execution = (this.toolQueues.get(call.threadId) ?? Promise.resolve()).catch(() => undefined).then(() => this.execute(call));
270
284
  this.toolQueues.set(call.threadId, execution);
271
285
  return execution;
@@ -284,7 +298,13 @@ export class CodexMissionSession {
284
298
  throw new Error(`Codex Mission requires an OpenAI model for ${agent}. Set model or roleModels explicitly.`);
285
299
  const effort = this.options.roleModels?.[agent]?.effort || this.options.effort || selection?.variant || embeddedEffort ||
286
300
  stored?.variant || /^variant: (.+)$/m.exec(header)?.[1];
287
- return { providerID: "openai", modelID: model, ...(effort ? { variant: effort } : {}) };
301
+ // The shared OpenCode alias is not a native Codex model. Keep its speed
302
+ // selection separate, including after shared Task routing and cold resume.
303
+ const fast = model === "gpt-6-luna-fast";
304
+ // A model/effort override does not silently change the independent native speed setting.
305
+ const serviceTier = fast ? "priority" : stored?.serviceTier;
306
+ return { providerID: "openai", modelID: fast ? "gpt-6-luna" : model,
307
+ ...(effort ? { variant: effort } : {}), ...(serviceTier ? { serviceTier } : {}) };
288
308
  }
289
309
  async reportPermissions(host, threadId) {
290
310
  await this.options.onEvent?.({ method: "sortie/permissions", threadId,
@@ -298,25 +318,29 @@ export class CodexMissionSession {
298
318
  if (!content)
299
319
  throw new Error(`Codex Mission role not supported: ${agent}`);
300
320
  const route = this.modelRoute(agent, undefined, selection);
301
- const model = route.modelID, effort = route.variant;
321
+ const model = route.modelID;
302
322
  const host = this.createHost();
303
323
  try {
304
324
  const auth = await host.authenticationState();
305
325
  if (auth.type !== "chatgpt")
306
326
  throw new Error("Codex Mission requires existing ChatGPT authentication.");
307
- const thread = await host.startThread({ cwd: this.directory, model, ephemeral: false, permissions: this.options.permissions,
327
+ const thread = await host.startThread({ cwd: this.directory, model, serviceTier: route.serviceTier, ephemeral: false, permissions: this.options.permissions,
308
328
  developerInstructions: content.replace(/^---\n[\s\S]*?\n---\n/, "") +
309
329
  "\nHost transport: use the supplied bash/read/write/task functions and sortie tools. They invoke the existing Mission hooks. " +
310
330
  "Use task to run or resume the returned native Task with its exact prompt and subagent_type. Tool failures are feedback for the same Mission. " +
311
331
  "Native shell and file operations outside these functions do not provide Mission validation evidence. " +
332
+ (this.options.executeCommand ? "Native apply_patch still uses the native sandbox, not the configured host executor; use the supplied write function for Mission edits. " : "") +
333
+ `The bash function runs ${this.shell.description}; supply commands in that shell's syntax. ` +
312
334
  "Declare formal validation as exact executable shell commands from the project root, without prose annotations such as (workdir: ...). " +
313
335
  "Use a root-relative command or an explicit shell cd for a subdirectory formal check. bash workdir changes that invocation only; " +
314
336
  "a command run in another directory does not validate a root-directory entry.",
315
337
  dynamicTools: this.tools(content), config: { "features.shell_tool": false, "features.unified_exec": false } });
316
338
  if (this.closed)
317
339
  throw new Error("Codex Mission adapter is closed.");
340
+ const serviceTier = host.threadServiceTier(thread) ?? route.serviceTier;
318
341
  const session = { id: thread, agent, parentID, outcome: "idle", host, history: [], usageBaseline: emptyCodexUsage(), createdAt: Date.now(),
319
- model: { providerID: "openai", modelID: host.threadModel(thread) ?? model, ...(effort ? { variant: effort } : {}) } };
342
+ model: { ...route, modelID: host.threadModel(thread) ?? model,
343
+ ...(serviceTier ? { serviceTier } : {}) } };
320
344
  this.sessions.set(thread, session);
321
345
  await this.reportPermissions(host, thread);
322
346
  return session;
@@ -366,9 +390,16 @@ export class CodexMissionSession {
366
390
  }
367
391
  await host.resumeThread(id, this.options.permissions);
368
392
  await this.reportPermissions(host, id);
393
+ const nativeModel = host.threadModel(id) ?? (typeof thread.model === "string" ? thread.model : undefined);
394
+ const observedTier = host.threadServiceTier(id), observedEffort = host.threadEffort(id);
395
+ // An observed native default is authoritative, not a missing value to fill
396
+ // from an older thread/read snapshot. Older hosts may omit these fields.
397
+ const nativeTier = observedTier === undefined ? (typeof thread.serviceTier === "string" ? thread.serviceTier : undefined) : observedTier;
398
+ const nativeEffort = observedEffort === undefined ? (typeof thread.reasoningEffort === "string" ? thread.reasoningEffort : undefined) : observedEffort;
369
399
  const session = { id, agent, parentID, outcome: turns.at(-1)?.status === "completed" ? "succeeded" : "interrupted", host, history: [], createdAt: codexNativeTime(thread.createdAt),
370
- model: this.modelRoute(agent, { providerID: String(thread.modelProvider), modelID: typeof thread.model === "string" ? thread.model : "unknown",
371
- ...(typeof thread.reasoningEffort === "string" ? { variant: thread.reasoningEffort } : {}) }) };
400
+ model: this.modelRoute(agent, nativeModel ? { providerID: String(thread.modelProvider), modelID: nativeModel,
401
+ ...(nativeTier ? { serviceTier: nativeTier } : {}),
402
+ ...(nativeEffort ? { variant: nativeEffort } : {}) } : undefined) };
372
403
  const lastUsage = this.observations.find(item => item.threadID === id && item.turnID === turns.at(-1)?.id && item.terminal);
373
404
  session.usageBaseline = codexUsageTotal(lastUsage?.total);
374
405
  if (lastUsage?.total)
@@ -421,9 +452,9 @@ export class CodexMissionSession {
421
452
  description: tool.description, inputSchema: toolSchema(tool.args) }));
422
453
  const define = (name, description, properties, required = Object.keys(properties)) => ({ type: "function", name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
423
454
  const string = { type: "string" };
424
- tools.push(define("bash", "Run a foreground command through the native Codex sandbox. Preserve formal validation commands exactly.", { command: string, workdir: string, timeout: { type: "integer", minimum: 1, maximum: 1200000 } }, ["command"]));
425
- tools.push(define("read", "Read a UTF-8 file through the native Codex sandbox.", { filePath: string }));
426
- tools.push(define("write", "Write a UTF-8 file through the native Codex sandbox.", { filePath: string, content: string }));
455
+ tools.push(define("bash", `Run a foreground ${this.shell.description} command through the configured executor. Preserve formal validation commands exactly.`, { command: string, workdir: string, timeout: { type: "integer", minimum: 1, maximum: 1200000 } }, ["command"]));
456
+ tools.push(define("read", "Read a UTF-8 file or list a directory's direct entries through the configured executor. Read directories to discover filenames instead of guessing them.", { filePath: string }));
457
+ tools.push(define("write", "Write a UTF-8 file through the configured executor and existing Mission hooks.", { filePath: string, content: string }));
427
458
  tools.push(define("task", "Run a returned Mission Task, or resume its existing task_id. Preserve the host's prompt and subagent_type.", { description: string, prompt: string, subagent_type: string, task_id: string, model: string, variant: string }, ["description", "prompt", "subagent_type"]));
428
459
  return tools;
429
460
  }
@@ -434,10 +465,10 @@ export class CodexMissionSession {
434
465
  const parts = [{ type: "text", text: prompt }];
435
466
  await this.hooks["chat.message"]?.({ sessionID: session.id, messageID, agent: session.agent }, { message, parts });
436
467
  // Shared Task routing may select a model; explicit adapter overrides still govern every native turn.
437
- if (this.options.model || this.options.effort || this.options.roleModels?.[session.agent])
438
- message.model = this.modelRoute(session.agent, message.model);
439
468
  if (message.model.providerID !== "openai")
440
469
  throw new Error("Codex Mission cannot use a non-OpenAI model route.");
470
+ message.model = this.modelRoute(session.agent, { ...message.model,
471
+ serviceTier: message.model.serviceTier ?? session.model.serviceTier });
441
472
  session.model = message.model;
442
473
  session.history.push({ info: { ...message, role: "user", sessionID: session.id }, parts });
443
474
  session.outcome = "running";
@@ -446,9 +477,13 @@ export class CodexMissionSession {
446
477
  const accounting = { info: { ...codexUsageInfo(observation), time: { created: startedAt } }, parts: [] };
447
478
  session.history.push(accounting);
448
479
  try {
480
+ await this.options.onEvent?.({ method: "sortie/modelRoute", threadId: session.id,
481
+ params: { agent: session.agent, model: session.model.modelID, effort: session.model.variant ?? null,
482
+ serviceTier: session.model.serviceTier ?? null } });
449
483
  await onDispatch?.(messageID);
450
484
  const result = await session.host.runTurn(session.id, parts.map(part => part.text).join("\n"), { cwd: this.directory, clientUserMessageId: messageID,
451
485
  model: message.model.modelID === "unknown" ? undefined : message.model.modelID, effort: message.model.variant ?? this.options.effort,
486
+ serviceTier: message.model.serviceTier,
452
487
  onEvent: event => {
453
488
  if (event.method === "thread/tokenUsage/updated" && object(event.params.tokenUsage)) {
454
489
  observation.turnID = typeof event.params.turnId === "string" ? event.params.turnId : observation.turnID;
@@ -506,6 +541,7 @@ export class CodexMissionSession {
506
541
  let dispatched = false;
507
542
  let child;
508
543
  let metadata;
544
+ let payload;
509
545
  try {
510
546
  if (call.tool === "task") {
511
547
  child = typeof args.task_id === "string" && args.task_id ? this.required(args.task_id) : undefined;
@@ -523,9 +559,11 @@ export class CodexMissionSession {
523
559
  metadata = { sessionId: child.id, status: result.status };
524
560
  }
525
561
  else {
526
- const command = call.tool === "bash" ? ["/bin/bash", "-c", String(args.command)]
527
- : call.tool === "read" ? [process.execPath, "-e", "process.stdout.write(require('node:fs').readFileSync(process.argv[1],'utf8'))", resolve(this.directory, String(args.filePath))]
528
- : [process.execPath, "-e", "require('node:fs').writeFileSync(process.argv[1],process.argv[2])", resolve(this.directory, String(args.filePath)), String(args.content)];
562
+ if (call.tool === "write")
563
+ payload = await prepareCodexWrite(resolve(this.directory, String(args.filePath)), String(args.content));
564
+ const command = call.tool === "bash" ? this.shell.command(String(args.command))
565
+ : call.tool === "read" ? [process.execPath, "-e", "const fs=require('node:fs'),p=process.argv[1];process.stdout.write(fs.statSync(p).isDirectory()?fs.readdirSync(p,{withFileTypes:true}).map(e=>e.name+(e.isDirectory()?'/':'')).sort().join('\\n')+'\\n':fs.readFileSync(p,'utf8'))", resolve(this.directory, String(args.filePath))]
566
+ : payload.command;
529
567
  const executor = this.options.executeCommand ? "host" : "native";
530
568
  await this.options.onEvent?.({ method: "sortie/commandExecution", threadId: session.id,
531
569
  params: { turnId: call.turnId, callId: call.callId, tool: call.tool, executor, cwd, status: "started" } });
@@ -586,6 +624,16 @@ export class CodexMissionSession {
586
624
  await this.stopHosts().catch(() => undefined);
587
625
  throw error;
588
626
  }
627
+ finally {
628
+ if (payload)
629
+ await payload.dispose().catch(async (error) => {
630
+ try {
631
+ await this.options.onEvent?.({ method: "sortie/payloadCleanup", threadId: session.id,
632
+ params: { callId: call.callId, status: "failed", reason: String(error) } });
633
+ }
634
+ catch { /* Cleanup reporting cannot replace the authoritative command outcome. */ }
635
+ });
636
+ }
589
637
  const time = { start, end: Date.now() };
590
638
  metadata.sortie_execution = time;
591
639
  session.history.push({ info: { id: call.callId, role: "assistant", sessionID: session.id, codexToolReceipt: true,
@@ -12,10 +12,11 @@ const commandText = (item) => {
12
12
  const powershellEnvelopePayload = (raw, trustedExecutable) => {
13
13
  if (!trustedExecutable)
14
14
  return undefined;
15
- // Windows Codex 0.160 reports a quoted absolute pwsh.exe plus one single-quoted
15
+ // Windows Codex 0.160/0.162 reports a quoted absolute pwsh.exe plus one single-quoted
16
16
  // -Command payload. It may protocol-escape the boundary quotes/backslashes.
17
- // Reject every other shell shape, nested quote, option, and trailing token.
18
- const match = /^(?:"|\\")([A-Za-z]:[\\/](?:[^"'\r\n]|\\\\)+[\\/]pwsh\.exe)(?:"|\\") -Command '([^'`\r\n]*)'$/iu.exec(raw);
17
+ // Only the observed optional -NoProfile flag is admitted. Other options,
18
+ // nested quotes and trailing tokens remain rejected.
19
+ const match = /^(?:"|\\")([A-Za-z]:[\\/](?:[^"'\r\n]|\\\\)+[\\/]pwsh\.exe)(?:"|\\") (?:-NoProfile )?-Command '([^'`\r\n]*)'$/iu.exec(raw);
19
20
  if (!match)
20
21
  return undefined;
21
22
  const identity = (value) => value.replaceAll("\\\\", "\\").replaceAll("/", "\\").toLowerCase();
@@ -0,0 +1,8 @@
1
+ export interface CodexMissionShell {
2
+ readonly description: string;
3
+ command(source: string): readonly string[];
4
+ }
5
+ /** Keep the source literal; only the host-owned exit trailer is appended. */
6
+ export declare function codexPowerShellCommand(executable: string, source: string): readonly string[];
7
+ /** Resolve the existing host shell, without installation or permission changes. */
8
+ export declare function resolveCodexMissionShell(trustedPowerShellExecutable?: string): Promise<CodexMissionShell>;
@@ -0,0 +1,22 @@
1
+ import { basename, isAbsolute } from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import { resolveValidationExecutable } from "../core/worktree-commit-artifact.js";
4
+ /** Keep the source literal; only the host-owned exit trailer is appended. */
5
+ export function codexPowerShellCommand(executable, source) {
6
+ return [executable, "-NoProfile", "-NonInteractive", "-Command", source +
7
+ "\nif (-not $?) { if ($LASTEXITCODE) { exit $LASTEXITCODE }; exit 1 }"];
8
+ }
9
+ /** Resolve the existing host shell, without installation or permission changes. */
10
+ export async function resolveCodexMissionShell(trustedPowerShellExecutable) {
11
+ if (process.platform !== "win32") {
12
+ if (trustedPowerShellExecutable)
13
+ throw new Error("--trusted-pwsh is only supported on Windows.");
14
+ return { description: "POSIX bash", command: source => ["/bin/bash", "-c", source] };
15
+ }
16
+ if (trustedPowerShellExecutable && (!isAbsolute(trustedPowerShellExecutable) || basename(trustedPowerShellExecutable).toLowerCase() !== "pwsh.exe"))
17
+ throw new Error("Codex Mission requires an absolute existing pwsh.exe path.");
18
+ const executable = await resolveValidationExecutable(trustedPowerShellExecutable ?? "pwsh.exe");
19
+ if (!executable || !(await stat(executable)).isFile())
20
+ throw new Error("Codex Mission requires existing PowerShell 7 (pwsh.exe). Select it with --trusted-pwsh; no shell is installed automatically.");
21
+ return { description: "Windows PowerShell 7 (not POSIX bash)", command: source => codexPowerShellCommand(executable, source) };
22
+ }
@@ -0,0 +1,5 @@
1
+ /** Stage transport data only. The configured executor still performs the project write. */
2
+ export declare function prepareCodexWrite(filePath: string, content: string): Promise<{
3
+ command: readonly string[];
4
+ dispose: () => Promise<void>;
5
+ }>;
@@ -0,0 +1,13 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { writeFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /** Stage transport data only. The configured executor still performs the project write. */
6
+ export async function prepareCodexWrite(filePath, content) {
7
+ const payload = join(tmpdir(), `sortie-codex-write-${randomUUID()}`);
8
+ await writeFile(payload, content, { encoding: "utf8", flag: "wx" });
9
+ return {
10
+ command: [process.execPath, "-e", "const fs=require('node:fs');fs.writeFileSync(process.argv[1],fs.readFileSync(process.argv[2]))", filePath, payload],
11
+ dispose: () => rm(payload, { force: true }),
12
+ };
13
+ }
@@ -165,6 +165,7 @@ export interface CodexMissionOwner {
165
165
  startTicks?: string;
166
166
  generation: string;
167
167
  closed?: boolean;
168
+ platform?: NodeJS.Platform;
168
169
  }
169
170
  export declare function codexProcessOwner(): Promise<CodexMissionOwner>;
170
171
  export declare function codexOwnerGone(owner: CodexMissionOwner | undefined): Promise<boolean>;
@@ -300,6 +301,11 @@ export declare function missionReviewAccepted(review: NonNullable<OperatorMissio
300
301
  /** A short native report, not a tag/hash-based second-review policy or an approval checklist. */
301
302
  export declare function missionSelfRecheckReport(text: string, source: string, hostBound?: boolean): Pick<MissionSelfRecheck, "unresolvedFindings" | "residualMajor"> | undefined;
302
303
  export declare function missionExecutionStatus(mission: OperatorMission): "not-required" | "not-started" | "running" | "execution-failed" | "executed";
304
+ /** A terminal failure is a collected result, not a successful operation. Required success
305
+ * remains in the declared formal checks and Operator's original-request comparison. */
306
+ export declare function missionExecutionComplete(mission: OperatorMission): boolean;
307
+ /** Projection of existing observations only; never another check or inferred success. */
308
+ export declare function missionOperationSummary(mission: OperatorMission): Record<string, unknown>;
303
309
  /** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
304
310
  * not process success. Unstructured commands retain their native exit semantics. */
305
311
  export declare function missionCommandOutcome(output: unknown, exit: unknown, status: unknown): {
@@ -27,7 +27,7 @@ async function codexProcessStart(pid) {
27
27
  return ticks;
28
28
  }
29
29
  export async function codexProcessOwner() {
30
- const owner = { pid: process.pid, generation: randomUUID() };
30
+ const owner = { pid: process.pid, generation: randomUUID(), platform: process.platform };
31
31
  if (process.platform === "linux") {
32
32
  owner.bootID = (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim();
33
33
  owner.startTicks = await codexProcessStart(process.pid);
@@ -39,7 +39,20 @@ export async function codexOwnerGone(owner) {
39
39
  return false;
40
40
  if (owner.closed)
41
41
  return true;
42
- if (process.platform !== "linux" || !owner.bootID || !owner.startTicks || !Number.isSafeInteger(owner.pid) || owner.pid < 1)
42
+ if (!Number.isSafeInteger(owner.pid) || owner.pid < 1 || owner.platform && owner.platform !== process.platform)
43
+ return false;
44
+ if (process.platform === "win32" && owner.platform === "win32") {
45
+ // Windows has no /proc identity. Only an absent PID proves this owner gone;
46
+ // PID reuse, access denial and legacy/unidentified owners stay unresolved.
47
+ try {
48
+ process.kill(owner.pid, 0);
49
+ return false;
50
+ }
51
+ catch (error) {
52
+ return error.code === "ESRCH";
53
+ }
54
+ }
55
+ if (process.platform !== "linux" || !owner.bootID || !owner.startTicks)
43
56
  return false;
44
57
  try {
45
58
  if ((await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim() !== owner.bootID)
@@ -103,14 +116,28 @@ export function missionExecutionStatus(mission) {
103
116
  if (mission.kind !== "operation")
104
117
  return "not-required";
105
118
  const execution = mission.execution;
106
- if (!execution || execution.observations.length === 0)
119
+ if (!execution || execution.commands.length === 0 || execution.observations.length === 0)
107
120
  return "not-started";
108
121
  const latest = execution.commands.map(command => [...execution.observations].reverse().find(item => item.command === command && item.directory === execution.directory));
109
122
  if (latest.some(item => item && !item.completedAt))
110
123
  return "running";
124
+ if (latest.some(item => !item || !["executed", "execution-failed"].includes(item.outcome ?? "")))
125
+ return "not-started";
111
126
  if (latest.some(item => item?.outcome === "execution-failed" || (item && (item.status === "error" || item.exit !== 0))))
112
127
  return "execution-failed";
113
- return latest.every(item => item?.outcome === "executed") ? "executed" : "not-started";
128
+ return "executed";
129
+ }
130
+ /** A terminal failure is a collected result, not a successful operation. Required success
131
+ * remains in the declared formal checks and Operator's original-request comparison. */
132
+ export function missionExecutionComplete(mission) {
133
+ return ["not-required", "executed", "execution-failed"].includes(missionExecutionStatus(mission));
134
+ }
135
+ /** Projection of existing observations only; never another check or inferred success. */
136
+ export function missionOperationSummary(mission) {
137
+ const status = missionExecutionStatus(mission);
138
+ return { kind: mission.kind ?? "implementation", status, terminal_complete: missionExecutionComplete(mission),
139
+ process_succeeded: status === "executed" ? true : status === "execution-failed" ? false : null,
140
+ ...(mission.execution ? { ...mission.execution } : {}) };
114
141
  }
115
142
  /** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
116
143
  * not process success. Unstructured commands retain their native exit semantics. */
@@ -267,7 +294,7 @@ export class OperatorMissionRuntime {
267
294
  async codexRecovery(root, action) {
268
295
  const lock = `${this.file(root)}.codex-recovery.lock`;
269
296
  const owner = await codexProcessOwner();
270
- const marker = `owner.${owner.pid}.${owner.bootID ?? "unknown"}.${owner.startTicks ?? "unknown"}.${owner.generation}`;
297
+ const marker = `owner.${owner.pid}.${owner.platform === "win32" ? "win32." : ""}${owner.bootID ?? "unknown"}.${owner.startTicks ?? "unknown"}.${owner.generation}`;
271
298
  for (let attempt = 0; attempt < 200; attempt++) {
272
299
  let acquired = false;
273
300
  const staging = `${lock}.${owner.generation}.tmp`;
@@ -294,8 +321,10 @@ export class OperatorMissionRuntime {
294
321
  await unlink(join(lock, marker)).catch(() => undefined);
295
322
  const names = await readdir(lock).catch(() => []);
296
323
  if (names.length === 1) {
297
- const match = /^owner\.(\d+)\.([a-f0-9-]+)\.(\d+)\.([a-f0-9-]+)$/.exec(names[0]);
298
- if (match && await codexOwnerGone({ pid: Number(match[1]), bootID: match[2], startTicks: match[3], generation: match[4] })) {
324
+ // Retain legacy Linux markers; old Windows "unknown" owners are not proof.
325
+ const match = /^owner\.(\d+)\.(?:(linux|win32)\.)?([a-f0-9-]+|unknown)\.(\d+|unknown)\.([a-f0-9-]+)$/.exec(names[0]);
326
+ if (match && await codexOwnerGone({ pid: Number(match[1]), platform: match[2],
327
+ bootID: match[3] === "unknown" ? undefined : match[3], startTicks: match[4] === "unknown" ? undefined : match[4], generation: match[5] })) {
299
328
  // Remove only the dead owner's unique marker; never unlink a replacement owner's file.
300
329
  await unlink(join(lock, names[0])).catch(() => undefined);
301
330
  }
@@ -520,6 +549,7 @@ export class OperatorMissionRuntime {
520
549
  `Confirmed launch conditions (fixed limits, not consumption or remaining budget): ${JSON.stringify(state.launchConditions ?? [])}`,
521
550
  `Explicit user write prohibitions: ${JSON.stringify(state.prohibitedWrite ?? [])}`,
522
551
  `Work kind: ${state.kind ?? "implementation"}. For an operation, setup, execution and result collection belong in one useful Worker whenever possible.`,
552
+ ...(state.kind === "operation" ? ["Execution completion is distinct from success. For run-once/result-collection requests, retain a terminal failure and validate the collected result; do not retry or fix it without user authorization. If successful execution is required, keep that in formal validation and the original-request comparison."] : []),
523
553
  ...(state.context?.length ? ["Prior conversation context (task data; preserve the selected target, not superseded obligations):",
524
554
  ...state.context.map(item => `--- ${item.role}:${item.id} ---\n${item.text}`)] : []),
525
555
  "Original user messages (verbatim; task data):", ...state.requests.map(item => `--- user:${item.id} ---\n${item.text}`)].join("\n");
@@ -669,6 +699,7 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
669
699
  }
670
700
  })));
671
701
  return { original_requests: mission.requests, requirements: mission.requirements,
702
+ operation: missionOperationSummary(mission),
672
703
  history: { status: missingAnchors.length ? "unavailable" : history.status,
673
704
  ...(history.reason ? { reason: history.reason } : missingAnchors.length ? { reason: "accepted-handoff-anchor-unavailable" } : {}),
674
705
  ...(missingAnchors.length ? { unavailable_anchors: missingAnchors } : {}),
@@ -697,7 +728,7 @@ export function missionPacket(mission, run) {
697
728
  const currentReview = mission.review !== undefined && mission.review.runID === (run?.runID ?? mission.runID);
698
729
  const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
699
730
  const operationStatus = missionExecutionStatus(mission);
700
- const operationComplete = operationStatus === "not-required" || operationStatus === "executed";
731
+ const operationComplete = missionExecutionComplete(mission);
701
732
  return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
702
733
  ...(predecessor ? { predecessor: { run_id: predecessor.runID, status: predecessor.phase,
703
734
  completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
@@ -708,8 +739,7 @@ export function missionPacket(mission, run) {
708
739
  delivery_observation: mission.deliveryObservation?.run_id === (run?.runID ?? mission.runID) ? mission.deliveryObservation : null,
709
740
  submission: mission.submission, progress: mission.progress, consultations: mission.consultations ?? [],
710
741
  attempts: mission.attempts ?? [], corrections: (mission.corrections ?? []).map(({ findings: _findings, initialPrompt: _prompt, ...item }) => item), ...(mission.rescue ? { rescue: mission.rescue } : {}),
711
- operation: { kind: mission.kind ?? "implementation", status: missionExecutionStatus(mission),
712
- ...(mission.execution ? { ...mission.execution } : {}) },
742
+ operation: missionOperationSummary(mission),
713
743
  execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
714
744
  running_units: run?.units.filter(unit => unit.status === "running").map(unit => ({ id: unit.unit.id, title: unit.unit.title, child_session_id: unit.childSessionID })) ?? [],
715
745
  pending_units: run?.units.filter(unit => unit.status === "pending").length ?? 0,
@@ -13,7 +13,7 @@ import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentati
13
13
  import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
14
14
  import { normalizeCommand, canonicalDeclaredValidationMembers, extractWritePaths } from "./gate.js";
15
15
  import { normalizeExecutionScope, normalizeManifestScope, normalizeRelativePath } from "../core/path.js";
16
- import { OperatorMissionRuntime, missionAcceptanceSummary, missionPacket, missionPlan, missionReviewAccepted, missionReviewIndependent, missionReviewScope, missionReviewTask, missionCommandOutcome, missionConversationContext, missionExecutionStatus, missionValidationCommand, missionReviewTraces, missionReviewVerdict, missionSelfRecheckReport } from "../core/operator-mission.js";
16
+ import { OperatorMissionRuntime, missionAcceptanceSummary, missionPacket, missionPlan, missionReviewAccepted, missionReviewIndependent, missionReviewScope, missionReviewTask, missionCommandOutcome, missionConversationContext, missionExecutionComplete, missionExecutionStatus, missionOperationSummary, missionValidationCommand, missionReviewTraces, missionReviewVerdict, missionSelfRecheckReport } from "../core/operator-mission.js";
17
17
  import { publishMissionProgress } from "./mission-progress.js";
18
18
  import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionDeliveryObservation, missionReviewBaseline, missionReviewSource, observedMissionValidation, observedMissionValidationSummary, missionReviewValidation, reviewerCorrectionValidation, reviewerCorrectionValidationFresh, validationDirectoryMatches } from "./mission-review.js";
19
19
  import { missionLocations, missionLocationPacket } from "./mission-location.js";
@@ -983,6 +983,12 @@ export function createProfiledPlugin(profile, assetVersion) {
983
983
  taskOwners.delete(mission.callID);
984
984
  return reconciled;
985
985
  }
986
+ function withMissionOperationOutcome(text, mission) {
987
+ if (!mission || missionExecutionStatus(mission) !== "execution-failed" || terminalRunOutcome(text) !== "DONE")
988
+ return text;
989
+ const detail = "Operation result: execution-failed (result accepted; process did not succeed).";
990
+ return text.includes(detail) ? text : text.replace(/^((?:[ \t]*\r?\n)*[^\r\n]+)/u, `$1 ${detail}`);
991
+ }
986
992
  function missionDispatchPacket(mission, run) {
987
993
  const packet = { ...missionPacket(mission, run), project_root: input.directory,
988
994
  coordinator_dispatch: mission.dispatchOpen ? "active" : ["completed", "cancelled"].includes(mission.phase) ? "terminal" : "resumable" };
@@ -1006,7 +1012,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1006
1012
  next_action: "Fast-lane: plan one useful Worker unit with the meaningful formal check known from user, project or task context and estimated scope, then dispatch its Task immediately. Investigation/edit/check/requested commit belong inside Worker before independent Review. Objective: target 2000 characters; original requests are supplied separately. Use Coordinator for an unknown check or real unit decomposition." };
1007
1013
  }
1008
1014
  if (mission.coordinator === null && mission.runID === run?.runID && !mission.dispatchOpen &&
1009
- run?.phase === "awaiting-acceptance" && mission.kind === "operation" && missionExecutionStatus(mission) !== "executed") {
1015
+ run?.phase === "awaiting-acceptance" && mission.kind === "operation" && !missionExecutionComplete(mission)) {
1010
1016
  return { ...packet, task: missions.task(mission),
1011
1017
  next_action: "Fast-lane operation is not executed. Dispatch this same mission's Coordinator Task to finish or report its actual blocker; a setup or validation success is not operation completion." };
1012
1018
  }
@@ -1442,6 +1448,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1442
1448
  budget_remaining_units: budget?.remaining_units ?? null,
1443
1449
  next_action: typeof packet.next_action === "string" ? packet.next_action : null,
1444
1450
  ...(record(packet.acceptance_summary) ? { observations: {
1451
+ operation: packet.acceptance_summary.operation,
1445
1452
  formal_validation: packet.acceptance_summary.formal_validation,
1446
1453
  native_declared_validation: packet.acceptance_summary.native_declared_validation,
1447
1454
  worker_terminals: (mission.attempts ?? []).map(attempt => ({ run_id: attempt.runID, status: attempt.status, terminal: attempt.terminal ?? null })),
@@ -1627,7 +1634,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1627
1634
  await requireRoot(context.sessionID);
1628
1635
  const state = await operators.required(context.sessionID);
1629
1636
  let mission = await missions.read(context.sessionID);
1630
- if (mission?.kind === "operation" && missionExecutionStatus(mission) !== "executed")
1637
+ if (mission?.kind === "operation" && !missionExecutionComplete(mission))
1631
1638
  return JSON.stringify({
1632
1639
  status: "not-ready", operation_status: missionExecutionStatus(mission),
1633
1640
  next_action: "The requested operation has not completed. Preserve its real outcome; auxiliary checks and a bounded review do not complete it. Continue the declared operation or report its blocker.",
@@ -1658,7 +1665,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1658
1665
  const reviewGaps = missionReportReview(mission, state.runID) === "evidence-gaps"
1659
1666
  ? mission.review.result?.trim() || "独立Reviewの未解決証拠あり" : undefined;
1660
1667
  const gapSummary = reviewGaps?.replace(/^EVIDENCE_GAPS\s*/u, "").replace(/\s+/gu, " ").slice(0, 500);
1661
- const text = `✅ **DONE** \`${state.runID}\` — declared checks passed; Operator accepted the result.\n\n` +
1668
+ const text = withMissionOperationOutcome(`✅ **DONE** \`${state.runID}\` — declared checks passed; Operator accepted the result.`, mission) + `\n\n` +
1662
1669
  `**変更点:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
1663
1670
  `**確認結果:** 宣言検証合格 — ${[...new Set(state.units.flatMap(unit => unit.unit.validation))].join("; ")}` +
1664
1671
  (mission?.review ? `\nレビュー: ${mission.review.verdict === "self-rechecked" ? "修正著者の自己再確認(独立PASSではない)" : mission.review.verdict === "evidence-gaps" ? "証拠不足を残して受入れ(レビューPASSではない)" : mission.review.verdict}.` : "") +
@@ -1670,6 +1677,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1670
1677
  panel = returnReportPanel(rendered);
1671
1678
  }
1672
1679
  return JSON.stringify({ status: result.status, run_id: state.runID,
1680
+ ...(mission?.kind === "operation" ? { operation: missionOperationSummary(mission) } : {}),
1673
1681
  acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null,
1674
1682
  ...(result.receipt?.status === "succeeded" && missionReportReview(mission, state.runID) === "evidence-gaps"
1675
1683
  ? { review_evidence_gaps: mission.review.result ?? null } : {}),
@@ -2032,9 +2040,13 @@ export function createProfiledPlugin(profile, assetVersion) {
2032
2040
  next_action: "Read operator_status: if the Coordinator dispatch is active, continue it; otherwise dispatch the returned task to resume the same Coordinator. Retain requirements, spend and candidate; no new mission or plan approval." });
2033
2041
  });
2034
2042
  } };
2035
- tools[startMission] = { description: `Operator: save current requirements; the host retains the original request verbatim. With a meaningful formal check known from user, project or task context and one useful unit, include unit NOW and dispatch its Worker directly; plan_units remains available after start. A literal command in the original request is not required. Worker owns investigation/edit/check/requested commit before independent Review; no routine preparation or commit-only handoff. ${MISSION_GIT_SCOPE} Unknown checks or real unit decomposition use Coordinator. intent=replace follows an actual user requirement change, retaining unchanged constraints and cumulative spend; for mission-source-reconciliation-required use the saved requirements when they reflect that change. intent=new is separate work in another location.`,
2043
+ const missionExecutionSchema = { type: "object", properties: { commands: stringList, directory: { type: "string" } },
2044
+ required: ["commands", "directory"], additionalProperties: false,
2045
+ description: "For an operation: the actual run/grade commands, not a preflight or NO_START check. Host observes their native shell completion separately from successful exits; no handwritten proof file is needed. For run-once/result collection, validate the collected result without rerunning the operation.", "x-sortie-optional": true };
2046
+ tools[startMission] = { description: `Operator: save current requirements; the host retains the original request verbatim. With a meaningful formal check known from user, project or task context and one useful unit, include unit NOW (and execution for an operation) and dispatch its Worker directly; plan_units remains available after start. A literal command in the original request is not required. Worker owns investigation/edit/check/requested commit before independent Review; no routine preparation or commit-only handoff. ${MISSION_GIT_SCOPE} Unknown checks or real unit decomposition use Coordinator. intent=replace follows an actual user requirement change, retaining unchanged constraints and cumulative spend; for mission-source-reconciliation-required use the saved requirements when they reflect that change. intent=new is separate work in another location.`,
2036
2047
  args: { requirements: { ...stringList, minItems: 1, maxItems: 64 },
2037
2048
  unit: { ...missionUnitSchema, description: "When the meaningful check and single-unit scope are already known, include this unit now. Returns its configured Worker directly, combining start_mission and plan_units without another model round trip.", "x-sortie-optional": true },
2049
+ execution: missionExecutionSchema,
2038
2050
  confirmed_conditions: conditionsSchema,
2039
2051
  prohibited_write: { ...stringList, description: "Only explicit path prohibitions from the user or applicable instructions. Never infer a parent glob from project/repository names, semantic 'do not modify the product', or the complement of estimated unit.write. Preserve the authorized clone and exact prohibited paths; semantic constraints stay in requirements.", "x-sortie-optional": true },
2040
2052
  kind: { type: "string", enum: ["implementation", "operation"], description: "Use operation for running an existing benchmark, command or procedure. The host records its actual execution separately from setup and checks.", "x-sortie-optional": true },
@@ -2123,10 +2135,16 @@ export function createProfiledPlugin(profile, assetVersion) {
2123
2135
  if (baseline)
2124
2136
  mission = await missions.update(context.sessionID, state => { state.reviewBaseline = baseline; });
2125
2137
  }
2126
- if (args.unit !== undefined && mission.runID === null && !mission.dispatchOpen && mission.kind !== "operation") {
2127
- const planned = JSON.parse(await declareMissionUnits(context.sessionID, context.sessionID, mission, [args.unit]));
2138
+ if (args.unit !== undefined && mission.runID === null && !mission.dispatchOpen &&
2139
+ (mission.kind !== "operation" || args.execution !== undefined || mission.execution)) {
2140
+ const planned = JSON.parse(await declareMissionUnits(context.sessionID, context.sessionID, mission, [args.unit], undefined, args.execution));
2128
2141
  return JSON.stringify({ ...locationObservation(context.sessionID), mission_id: mission.id, requirements: mission.requirements, ...planned });
2129
2142
  }
2143
+ if (args.execution !== undefined && mission.runID === null && !mission.dispatchOpen) {
2144
+ const operation = declaredMissionExecution(mission, args.execution);
2145
+ if (operation)
2146
+ mission = await missions.update(context.sessionID, state => { state.kind = "operation"; state.execution = operation; });
2147
+ }
2130
2148
  return JSON.stringify({ ...locationObservation(context.sessionID), ...(mission.dispatchOpen
2131
2149
  ? missionDispatchPacket(mission, await operators.read(context.sessionID))
2132
2150
  : { mission_id: mission.id, requirements: mission.requirements, task: missions.task(mission),
@@ -2267,6 +2285,19 @@ export function createProfiledPlugin(profile, assetVersion) {
2267
2285
  obligations: policy.proposal.obligations }, run_id: prepared.runID, unit_id: unitID,
2268
2286
  task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
2269
2287
  } };
2288
+ function declaredMissionExecution(mission, execution) {
2289
+ const operation = mission.execution;
2290
+ // Optional empty operation fields must not turn a normal edit into an operation.
2291
+ if (execution === undefined || mission.kind === "implementation" && record(execution) &&
2292
+ Array.isArray(execution.commands) && execution.commands.length === 0)
2293
+ return operation;
2294
+ if (!record(execution) || typeof execution.directory !== "string" || !execution.directory.trim() ||
2295
+ !Array.isArray(execution.commands) || execution.commands.length === 0 || execution.commands.length > 16 ||
2296
+ !execution.commands.every(command => typeof command === "string" && command.trim()))
2297
+ throw new Error("mission-operation-input: supply the actual operation commands and their working directory");
2298
+ const commands = [...new Set(execution.commands.map(missionValidationCommand).map(normalizeCommand))];
2299
+ return { commands, directory: resolve(input.directory, execution.directory), observations: operation?.observations ?? [] };
2300
+ }
2270
2301
  async function declareMissionUnits(root, actor, mission, raw, reason, execution, planningCallID) {
2271
2302
  await control.currentBudget(root);
2272
2303
  mission = await missions.required(root);
@@ -2281,21 +2312,11 @@ export function createProfiledPlugin(profile, assetVersion) {
2281
2312
  `requirements to Operator. Operator can relink this mission in place via ${startMission} intent=replace ` +
2282
2313
  `with those exact requirements when they match the user's changed scope; no Worker can start before that decision.` });
2283
2314
  mission = await retainCancelledMissionAcceptance(root, mission, previous);
2284
- let operation = mission.execution;
2315
+ const operation = declaredMissionExecution(mission, execution);
2285
2316
  // Models can serialize an optional operation field as an empty object for a normal edit.
2286
2317
  // Do not turn an implementation mission into an operation (or block its first Worker).
2287
2318
  const emptyImplementationExecution = mission.kind === "implementation" && record(execution) &&
2288
2319
  Array.isArray(execution.commands) && execution.commands.length === 0;
2289
- if (execution !== undefined && !emptyImplementationExecution) {
2290
- if (!record(execution) || typeof execution.directory !== "string" || !execution.directory.trim() ||
2291
- !Array.isArray(execution.commands) || execution.commands.length === 0 || execution.commands.length > 16 ||
2292
- !execution.commands.every(command => typeof command === "string" && command.trim()))
2293
- throw new Error("mission-operation-input: supply the actual operation commands and their working directory");
2294
- const commands = [...new Set(execution.commands.map(missionValidationCommand).map(normalizeCommand))];
2295
- const directory = resolve(input.directory, execution.directory);
2296
- // A corrected command is allowed on replan; retain prior observations for accounting.
2297
- operation = { commands, directory, observations: operation?.observations ?? [] };
2298
- }
2299
2320
  const plan = missionPlan(mission, raw, input.directory);
2300
2321
  assertMissionWritePaths(mission, plan.units.flatMap(unit => unit.write));
2301
2322
  if (actor === root && (mission.coordinator !== null || plan.units.length !== 1))
@@ -2462,9 +2483,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2462
2483
  }
2463
2484
  tools[planUnits] = { description: `Coordinator or single-unit Fast-lane Operator: declare useful work with estimated read/write scope and meaningful formal checks. executor=self starts implementation/correction and formal validation HERE without a Worker or handoff read; otherwise dispatch the returned configured Worker promptly. Keep investigation/edit/check/requested commit with the same author before independent Review, not a commit-only handoff. ${MISSION_GIT_SCOPE} Native scope reconciliation/expand_unit retain host permissions and explicit path prohibitions. Objective is the target or corrective delta: aim for 2000 characters; the full original request/public reproduction is supplied separately, not copied here. Oversized unit instructions are retained verbatim in handoff Mission context, not rejected for another planning round. Keep every requirement covered. Final validation proves the unit; empty or dummy checks do not qualify. Diagnostics need no registration. write: [] is read-only; dir/** and native absolute paths support actual outputs. reason replans settled work, or ends your own quiescent direct unit without acceptance to correct its declaration, in the same requirements/budget; same-scope failed validation uses retry_mission_unit. Reviewer FINDINGS stay with the same Reviewer for correction, formal validation and self-recheck.`,
2464
2485
  args: { units: { type: "array", minItems: 1, maxItems: 32, items: missionUnitSchema },
2465
- execution: { type: "object", properties: { commands: stringList, directory: { type: "string" } },
2466
- required: ["commands", "directory"], additionalProperties: false,
2467
- description: "For an operation: the actual run/grade commands, not a preflight or NO_START check. Host observes their native shell completion; no handwritten proof file is needed.", "x-sortie-optional": true },
2486
+ execution: missionExecutionSchema,
2468
2487
  reason: { type: "string", "x-sortie-optional": true },
2469
2488
  executor: { type: "string", enum: ["worker", "self"], description: "Use self to implement/correct and formally validate here without a Worker handoff. Default worker retains configured Worker routing.", "x-sortie-optional": true } }, execute: async (args, context) => {
2470
2489
  const { root, mission } = await missionAuthority(context.sessionID);
@@ -2870,7 +2889,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2870
2889
  args: { status: { type: "string", enum: ["ready", "needs-decision", "blocked"] }, summary: stringSchema }, execute: async (args, context) => {
2871
2890
  const { root, mission } = await missionAuthority(context.sessionID);
2872
2891
  if (args.status === "ready") {
2873
- if (mission.kind === "operation" && missionExecutionStatus(mission) !== "executed") {
2892
+ if (mission.kind === "operation" && !missionExecutionComplete(mission)) {
2874
2893
  return JSON.stringify({ ...missionPacket(mission, await operators.read(root)), status: "operation-incomplete",
2875
2894
  next_action: missionExecutionStatus(mission) === "running"
2876
2895
  ? "The declared operation is already running. Inspect its native shell/progress; do not start another Worker or run. Wait for a terminal result, or report the existing run as blocked if its completion cannot be observed."
@@ -2890,7 +2909,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2890
2909
  ...missionPacket(updated, submittedRun),
2891
2910
  next_action: "Return this result and acceptance_summary to Operator now. Compare the original requests with the candidate and actual evidence; inspect concrete gaps only, then complete_mission if satisfied. No routine archive search or full source reread. Completion still requires its final comparison and complete_mission receipt." });
2892
2911
  } };
2893
- tools[completeMission] = { description: "Operator only: after comparing the original request, source, actual checks and current review disposition (explicit native author self-recheck is not independent PASS), explicitly accept the whole mission. Returns the measured 🐾 report on success; never treat Worker start, CORRECTION_READY or one passing check as completion.",
2912
+ tools[completeMission] = { description: "Operator only: after comparing the original request, source, actual checks and current review disposition (explicit native author self-recheck is not independent PASS), explicitly accept the whole mission. A collected terminal failure can satisfy a run-once/report request, not a request for successful execution. Returns operation outcome separately from the measured 🐾 Mission receipt; never treat Worker start, CORRECTION_READY or one passing check as completion.",
2894
2913
  args: {}, execute: async (_args, context) => {
2895
2914
  await requireRoot(context.sessionID);
2896
2915
  await missions.required(context.sessionID);
@@ -2941,7 +2960,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2941
2960
  return;
2942
2961
  const run = await operators.read(sessionID);
2943
2962
  const mission = await missions.read(sessionID);
2944
- const text = await control.renderReturnReport(sessionID, decoratePreviewHeadings(part.text), goalFingerprint(receipt), run ? missionReportReview(mission, run.runID) : undefined, run ? missionReportReviewGaps(mission, run.runID) : undefined);
2963
+ const text = await control.renderReturnReport(sessionID, withMissionOperationOutcome(decoratePreviewHeadings(part.text), mission), goalFingerprint(receipt), run ? missionReportReview(mission, run.runID) : undefined, run ? missionReportReviewGaps(mission, run.runID) : undefined);
2945
2964
  if (text === undefined)
2946
2965
  return;
2947
2966
  if (text === part.text) {
@@ -3606,7 +3625,9 @@ export function createProfiledPlugin(profile, assetVersion) {
3606
3625
  }
3607
3626
  if (ownership?.mission) {
3608
3627
  const current = await missions.required(ownership.root);
3609
- if (current.callID !== request.callID) {
3628
+ // Native completion can arrive after cancellation/acceptance or an agent switch.
3629
+ // Close its historical owner without reviving the Mission or requiring revoked root authority.
3630
+ if (current.callID !== request.callID || ["completed", "cancelled"].includes(current.phase)) {
3610
3631
  taskOwners.delete(request.callID);
3611
3632
  return;
3612
3633
  }
@@ -3941,7 +3962,11 @@ export function createProfiledPlugin(profile, assetVersion) {
3941
3962
  const role = (await identity(request.sessionID)).role;
3942
3963
  if (role === "dog-operator" || !await rootFor(request.sessionID))
3943
3964
  return;
3944
- const forwardedText = role === "dog-coordinator" ? forwardTerminalText(output.text) : output.text;
3965
+ let forwardedText = role === "dog-coordinator" ? forwardTerminalText(output.text) : output.text;
3966
+ if (role === "dog-coordinator" && terminalRunOutcome(forwardedText) === "DONE" &&
3967
+ (await control.currentReceipt(request.sessionID))?.status === "succeeded") {
3968
+ forwardedText = withMissionOperationOutcome(forwardedText, await missions.read(request.sessionID));
3969
+ }
3945
3970
  // Ordinary Operator chat is not a request for a historical Mission return panel.
3946
3971
  if (role === "dog-coordinator" && terminalRunOutcome(forwardedText) === undefined)
3947
3972
  return;
@@ -54,6 +54,11 @@ not its exit; it cannot complete the declared operation. This does not restrict
54
54
  For a declared operation, do not append a tee pipeline, redirection or wrapper that was not declared. Save output separately
55
55
  when needed. Use that same run's terminal state and result for completion. If a run already started,
56
56
  inspect its progress and report a missing terminal observation instead of launching it again.
57
+ Execution completion is not successful execution. For a run-once/result-collection request, preserve
58
+ a terminal failure, validate the collected result and return it without a repair loop or another run.
59
+ Successful execution requirements stay in meaningful formal validation and Operator's comparison
60
+ with the original request. Mission acceptance must report the operation's exit/outcome separately;
61
+ a succeeded Mission receipt never changes a failed benchmark into a successful benchmark.
57
62
  `;
58
63
  function controls(profile, names) {
59
64
  return names.map(name => ` ${profile.toolPrefix}${name}: true`).join("\n");
@@ -114,7 +119,8 @@ quality threshold and explicit model/budget choice. Follow AGENTS.md and use the
114
119
  ${profile.toolPrefix}start_mission with intent: "replace" and the saved requirements. The cancelled
115
120
  Mission is archived; cumulative spend is retained. Dispatch only the returned Coordinator Task.
116
121
  2. If a meaningful formal check is known from the user, project or task context and the work fits one unit,
117
- include unit in start_mission to receive its Worker immediately in the same call. If already started,
122
+ include unit in start_mission to receive its Worker immediately in the same call. For a known operation,
123
+ include execution commands/directory alongside unit. If already started,
118
124
  call plan_units and dispatch its Worker. Source investigation, shell/environment checks,
119
125
  fix design and output inventory belong inside that Worker, not a routine Operator preflight.
120
126
  Use estimated read/write paths; native scope reconciliation and expand_unit cover actual outputs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.13.9",
3
+ "version": "0.13.11",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",