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.
- package/.agents/skills/sortie-dogs/SKILL.md +9 -3
- package/.agents/skills/sortie-dogs/sortie-dogs.version +1 -1
- package/README.md +49 -18
- package/dist/asset-version.d.ts +2 -2
- package/dist/asset-version.js +2 -2
- package/dist/cli/codex-mission.d.ts +1 -1
- package/dist/cli/codex-mission.js +9 -3
- package/dist/codex/app-server.d.ts +9 -0
- package/dist/codex/app-server.js +24 -3
- package/dist/codex/mission-session.d.ts +4 -0
- package/dist/codex/mission-session.js +65 -17
- package/dist/codex/mission-settlement.js +4 -3
- package/dist/codex/mission-shell.d.ts +8 -0
- package/dist/codex/mission-shell.js +22 -0
- package/dist/codex/mission-write.d.ts +5 -0
- package/dist/codex/mission-write.js +13 -0
- package/dist/core/operator-mission.d.ts +6 -0
- package/dist/core/operator-mission.js +40 -10
- package/dist/plugin/profiled.js +51 -26
- package/dist/runtime-mission-assets.js +7 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
39
|
-
([release notes](docs/release-v0.13.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
91
|
-
|
|
92
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
|
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`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -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.
|
|
7
|
-
export declare const CODEX_SKILL_ASSET_VERSION = "0.13.
|
|
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;
|
package/dist/asset-version.js
CHANGED
|
@@ -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.
|
|
7
|
-
export const CODEX_SKILL_ASSET_VERSION = "0.13.
|
|
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"),
|
|
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. */
|
package/dist/codex/app-server.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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:
|
|
371
|
-
...(
|
|
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",
|
|
425
|
-
tools.push(define("read", "Read a UTF-8 file through the
|
|
426
|
-
tools.push(define("write", "Write a UTF-8 file through the
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
//
|
|
18
|
-
|
|
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,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 (
|
|
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
|
|
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
|
-
|
|
298
|
-
|
|
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 =
|
|
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:
|
|
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,
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -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" &&
|
|
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" &&
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
2127
|
-
|
|
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
|
-
|
|
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:
|
|
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" &&
|
|
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 🐾
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|