sortie-dogs 0.13.9 → 0.13.10

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
@@ -289,7 +303,19 @@ existing local bridge loads enforcement and model routing. OpenCode can reload w
289
303
  but replacing an installed dependency may require a full restart. A new chat session alone does not
290
304
  prove the newly installed plugin is loaded.
291
305
 
292
- ## v0.13.9 runtime updates
306
+ ## v0.13.10 runtime updates
307
+
308
+ PR #169 connects natural-language Windows Codex Missions to the same multi-role lifecycle using
309
+ existing PowerShell 7, preserves command failure exits and native resume settings, and maps the
310
+ Luna-fast Worker alias to native Luna/max with the separate priority tier. Directory reads list
311
+ direct entries instead of requiring filename guesses. OpenCode model settings and native permission
312
+ profiles remain unchanged. PR #168 provides one host-aware Anko runner, reuses unchanged setup,
313
+ removes mandatory paid diagnostic probes, and preserves prior attempts, unknown usage and service
314
+ evidence. Its recorded v0.13.9 Anko completion and PR #169's Windows Codex host-executor completion
315
+ are historical fixed-candidate observations, not new v0.13.10 benchmark or native sandbox results.
316
+ See [release notes](docs/release-v0.13.10.md).
317
+
318
+ ## v0.13.9 runtime updates (retained)
293
319
 
294
320
  PRs #163–#166 connect native Codex sessions to the existing Mission lifecycle, add the explicit
295
321
  `$sortie-dogs` skill and `codex init`, restore operation Reviewer correction within existing write
@@ -654,7 +680,7 @@ register stable and `v010` from the same package installation path in one host.
654
680
  Project-local installation is recommended. To expose the current Mission assets globally:
655
681
 
656
682
  ```sh
657
- npm install --global sortie-dogs@0.13.9
683
+ npm install --global sortie-dogs@0.13.10
658
684
  sortie-dogs init --global --profile v010
659
685
  ```
660
686
 
@@ -666,7 +692,7 @@ can resolve a **separate dependency** under that config root. Updating npm-globa
666
692
  it. For that layout, also install the same release at the actual config root, then rerun global init:
667
693
 
668
694
  ```sh
669
- npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.9
695
+ npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.10
670
696
  sortie-dogs init --global --profile v010
671
697
  ```
672
698
 
@@ -689,7 +715,7 @@ version, preserves user configuration, and stops safely on unknown ownership or
689
715
  conflicting files.
690
716
 
691
717
  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
718
+ marker of `0.13.10-codex-windows-v1` identifies the assets; it does not prove an already-running
693
719
  OpenCode process has reloaded the plugin.
694
720
 
695
721
  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.10-codex-windows-v1";
7
+ export declare const CODEX_SKILL_ASSET_VERSION = "0.13.10-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.10-codex-windows-v1";
7
+ export const CODEX_SKILL_ASSET_VERSION = "0.13.10-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;
@@ -8,10 +8,12 @@ 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";
11
12
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12
13
  /** Native Codex transport for the existing Mission tools, roles and acceptance state. */
13
14
  export class CodexMissionSession {
14
15
  options;
16
+ shell;
15
17
  hooks;
16
18
  sessions = new Map();
17
19
  closed = false;
@@ -23,12 +25,13 @@ export class CodexMissionSession {
23
25
  ownedRoots = new Set();
24
26
  toolQueues = new Map();
25
27
  directory;
26
- constructor(options) {
28
+ constructor(options, shell) {
27
29
  this.options = options;
30
+ this.shell = shell;
28
31
  this.directory = resolve(options.projectRoot);
29
32
  }
30
33
  static async create(options) {
31
- const adapter = new CodexMissionSession(options);
34
+ const adapter = new CodexMissionSession(options, await resolveCodexMissionShell(options.trustedPowerShellExecutable));
32
35
  adapter.owner = await codexProcessOwner();
33
36
  const id = (request) => request.path.id;
34
37
  const info = (session) => ({ id: session.id, agent: session.agent, parentID: session.parentID,
@@ -284,7 +287,13 @@ export class CodexMissionSession {
284
287
  throw new Error(`Codex Mission requires an OpenAI model for ${agent}. Set model or roleModels explicitly.`);
285
288
  const effort = this.options.roleModels?.[agent]?.effort || this.options.effort || selection?.variant || embeddedEffort ||
286
289
  stored?.variant || /^variant: (.+)$/m.exec(header)?.[1];
287
- return { providerID: "openai", modelID: model, ...(effort ? { variant: effort } : {}) };
290
+ // The shared OpenCode alias is not a native Codex model. Keep its speed
291
+ // selection separate, including after shared Task routing and cold resume.
292
+ const fast = model === "gpt-6-luna-fast";
293
+ // A model/effort override does not silently change the independent native speed setting.
294
+ const serviceTier = fast ? "priority" : stored?.serviceTier;
295
+ return { providerID: "openai", modelID: fast ? "gpt-6-luna" : model,
296
+ ...(effort ? { variant: effort } : {}), ...(serviceTier ? { serviceTier } : {}) };
288
297
  }
289
298
  async reportPermissions(host, threadId) {
290
299
  await this.options.onEvent?.({ method: "sortie/permissions", threadId,
@@ -298,25 +307,29 @@ export class CodexMissionSession {
298
307
  if (!content)
299
308
  throw new Error(`Codex Mission role not supported: ${agent}`);
300
309
  const route = this.modelRoute(agent, undefined, selection);
301
- const model = route.modelID, effort = route.variant;
310
+ const model = route.modelID;
302
311
  const host = this.createHost();
303
312
  try {
304
313
  const auth = await host.authenticationState();
305
314
  if (auth.type !== "chatgpt")
306
315
  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,
316
+ const thread = await host.startThread({ cwd: this.directory, model, serviceTier: route.serviceTier, ephemeral: false, permissions: this.options.permissions,
308
317
  developerInstructions: content.replace(/^---\n[\s\S]*?\n---\n/, "") +
309
318
  "\nHost transport: use the supplied bash/read/write/task functions and sortie tools. They invoke the existing Mission hooks. " +
310
319
  "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
320
  "Native shell and file operations outside these functions do not provide Mission validation evidence. " +
321
+ (this.options.executeCommand ? "Native apply_patch still uses the native sandbox, not the configured host executor; use the supplied write function for Mission edits. " : "") +
322
+ `The bash function runs ${this.shell.description}; supply commands in that shell's syntax. ` +
312
323
  "Declare formal validation as exact executable shell commands from the project root, without prose annotations such as (workdir: ...). " +
313
324
  "Use a root-relative command or an explicit shell cd for a subdirectory formal check. bash workdir changes that invocation only; " +
314
325
  "a command run in another directory does not validate a root-directory entry.",
315
326
  dynamicTools: this.tools(content), config: { "features.shell_tool": false, "features.unified_exec": false } });
316
327
  if (this.closed)
317
328
  throw new Error("Codex Mission adapter is closed.");
329
+ const serviceTier = host.threadServiceTier(thread) ?? route.serviceTier;
318
330
  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 } : {}) } };
331
+ model: { ...route, modelID: host.threadModel(thread) ?? model,
332
+ ...(serviceTier ? { serviceTier } : {}) } };
320
333
  this.sessions.set(thread, session);
321
334
  await this.reportPermissions(host, thread);
322
335
  return session;
@@ -366,9 +379,16 @@ export class CodexMissionSession {
366
379
  }
367
380
  await host.resumeThread(id, this.options.permissions);
368
381
  await this.reportPermissions(host, id);
382
+ const nativeModel = host.threadModel(id) ?? (typeof thread.model === "string" ? thread.model : undefined);
383
+ const observedTier = host.threadServiceTier(id), observedEffort = host.threadEffort(id);
384
+ // An observed native default is authoritative, not a missing value to fill
385
+ // from an older thread/read snapshot. Older hosts may omit these fields.
386
+ const nativeTier = observedTier === undefined ? (typeof thread.serviceTier === "string" ? thread.serviceTier : undefined) : observedTier;
387
+ const nativeEffort = observedEffort === undefined ? (typeof thread.reasoningEffort === "string" ? thread.reasoningEffort : undefined) : observedEffort;
369
388
  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 } : {}) }) };
389
+ model: this.modelRoute(agent, nativeModel ? { providerID: String(thread.modelProvider), modelID: nativeModel,
390
+ ...(nativeTier ? { serviceTier: nativeTier } : {}),
391
+ ...(nativeEffort ? { variant: nativeEffort } : {}) } : undefined) };
372
392
  const lastUsage = this.observations.find(item => item.threadID === id && item.turnID === turns.at(-1)?.id && item.terminal);
373
393
  session.usageBaseline = codexUsageTotal(lastUsage?.total);
374
394
  if (lastUsage?.total)
@@ -421,9 +441,9 @@ export class CodexMissionSession {
421
441
  description: tool.description, inputSchema: toolSchema(tool.args) }));
422
442
  const define = (name, description, properties, required = Object.keys(properties)) => ({ type: "function", name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
423
443
  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 }));
444
+ 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"]));
445
+ 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 }));
446
+ tools.push(define("write", "Write a UTF-8 file through the configured executor and existing Mission hooks.", { filePath: string, content: string }));
427
447
  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
448
  return tools;
429
449
  }
@@ -434,10 +454,10 @@ export class CodexMissionSession {
434
454
  const parts = [{ type: "text", text: prompt }];
435
455
  await this.hooks["chat.message"]?.({ sessionID: session.id, messageID, agent: session.agent }, { message, parts });
436
456
  // 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
457
  if (message.model.providerID !== "openai")
440
458
  throw new Error("Codex Mission cannot use a non-OpenAI model route.");
459
+ message.model = this.modelRoute(session.agent, { ...message.model,
460
+ serviceTier: message.model.serviceTier ?? session.model.serviceTier });
441
461
  session.model = message.model;
442
462
  session.history.push({ info: { ...message, role: "user", sessionID: session.id }, parts });
443
463
  session.outcome = "running";
@@ -446,9 +466,13 @@ export class CodexMissionSession {
446
466
  const accounting = { info: { ...codexUsageInfo(observation), time: { created: startedAt } }, parts: [] };
447
467
  session.history.push(accounting);
448
468
  try {
469
+ await this.options.onEvent?.({ method: "sortie/modelRoute", threadId: session.id,
470
+ params: { agent: session.agent, model: session.model.modelID, effort: session.model.variant ?? null,
471
+ serviceTier: session.model.serviceTier ?? null } });
449
472
  await onDispatch?.(messageID);
450
473
  const result = await session.host.runTurn(session.id, parts.map(part => part.text).join("\n"), { cwd: this.directory, clientUserMessageId: messageID,
451
474
  model: message.model.modelID === "unknown" ? undefined : message.model.modelID, effort: message.model.variant ?? this.options.effort,
475
+ serviceTier: message.model.serviceTier,
452
476
  onEvent: event => {
453
477
  if (event.method === "thread/tokenUsage/updated" && object(event.params.tokenUsage)) {
454
478
  observation.turnID = typeof event.params.turnId === "string" ? event.params.turnId : observation.turnID;
@@ -523,8 +547,8 @@ export class CodexMissionSession {
523
547
  metadata = { sessionId: child.id, status: result.status };
524
548
  }
525
549
  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))]
550
+ const command = call.tool === "bash" ? this.shell.command(String(args.command))
551
+ : 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))]
528
552
  : [process.execPath, "-e", "require('node:fs').writeFileSync(process.argv[1],process.argv[2])", resolve(this.directory, String(args.filePath)), String(args.content)];
529
553
  const executor = this.options.executeCommand ? "host" : "native";
530
554
  await this.options.onEvent?.({ method: "sortie/commandExecution", threadId: session.id,
@@ -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
+ }
@@ -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>;
@@ -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)
@@ -267,7 +280,7 @@ export class OperatorMissionRuntime {
267
280
  async codexRecovery(root, action) {
268
281
  const lock = `${this.file(root)}.codex-recovery.lock`;
269
282
  const owner = await codexProcessOwner();
270
- const marker = `owner.${owner.pid}.${owner.bootID ?? "unknown"}.${owner.startTicks ?? "unknown"}.${owner.generation}`;
283
+ const marker = `owner.${owner.pid}.${owner.platform === "win32" ? "win32." : ""}${owner.bootID ?? "unknown"}.${owner.startTicks ?? "unknown"}.${owner.generation}`;
271
284
  for (let attempt = 0; attempt < 200; attempt++) {
272
285
  let acquired = false;
273
286
  const staging = `${lock}.${owner.generation}.tmp`;
@@ -294,8 +307,10 @@ export class OperatorMissionRuntime {
294
307
  await unlink(join(lock, marker)).catch(() => undefined);
295
308
  const names = await readdir(lock).catch(() => []);
296
309
  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] })) {
310
+ // Retain legacy Linux markers; old Windows "unknown" owners are not proof.
311
+ const match = /^owner\.(\d+)\.(?:(linux|win32)\.)?([a-f0-9-]+|unknown)\.(\d+|unknown)\.([a-f0-9-]+)$/.exec(names[0]);
312
+ if (match && await codexOwnerGone({ pid: Number(match[1]), platform: match[2],
313
+ bootID: match[3] === "unknown" ? undefined : match[3], startTicks: match[4] === "unknown" ? undefined : match[4], generation: match[5] })) {
299
314
  // Remove only the dead owner's unique marker; never unlink a replacement owner's file.
300
315
  await unlink(join(lock, names[0])).catch(() => undefined);
301
316
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.13.9",
3
+ "version": "0.13.10",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",