Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ It is designed for tasks where the parent should stay in supervisor mode, split
## Requirements

- A Codex environment with `codex exec` available on `PATH`
- PowerShell
- PowerShell (`pwsh` on Linux/macOS, Windows PowerShell or `pwsh` on Windows)
- A workspace where local skills under `./skills` are supported

This repository is already arranged as a Codex workspace. The root `AGENTS.md` wires `/sub` requests to the local orchestrator skill.
Expand Down Expand Up @@ -73,6 +73,14 @@ Copy-Item `
-AsJson
```

On Ubuntu or other Linux environments, run the same launcher with `pwsh`:

```bash
pwsh -File ./skills/codex-subagent-orchestrator/scripts/start-codex-subagent-team.ps1 \
-SpecPath ./skills/codex-subagent-orchestrator/assets/spec-templates/minimal-write.template.json \
-AsJson
```

The template uses:

- `cwd: "."`
Expand Down Expand Up @@ -224,6 +232,8 @@ From the skill contract:

That means the user should not need separate chat commands such as `/sub-team` or `/sub-queue`. The parent Codex instance is expected to decide automatically from context.

On Linux and macOS, `/sub` should prefer the launcher path when `pwsh` is available. If the task is a bounded one-off request and no PowerShell host is available, the parent should fall back to direct `codex exec` rather than fail the `/sub` request outright.

## How the Skill Is Intended to Work

Parent Codex responsibilities:
Expand Down
4 changes: 3 additions & 1 deletion skills/codex-subagent-orchestrator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ Use `scripts/start-codex-subagent-team.ps1` with a JSON spec when:
- you want per-worker reasoning, sandbox, or output control
- you want prompt files, `last.txt`, and a manifest for later supervision or forensics

If the request is simple enough, you may still invoke `codex exec` directly without the launcher.
On Linux and macOS, run the launcher with `pwsh`.

If the request is simple enough, you may still invoke `codex exec` directly without the launcher. Prefer that fallback for one-off `/sub` tasks when no PowerShell host is available.

Use `scripts/start-codex-subagent-queue.ps1` with a queue config when:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ If you want to carry tracker payloads from another system, prefer writing them t

The optional top-level `hooks` object currently supports a Symphony-style one-shot bootstrap:

- `after_create`: PowerShell command to run before workers when bootstrap is needed
- `after_create`: PowerShell command to run before workers when bootstrap is needed; the launcher executes it with the current PowerShell host (`pwsh` on Linux/macOS, PowerShell on Windows)
- `after_create_sentinel_paths`: if any listed path is missing, the hook runs
- `after_create_if_workspace_empty`: when true, the hook also runs if the workspace root has no files
- `after_create_stdout_file`: optional path for captured stdout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ Workers produce bounded outputs. The parent integrates, validates, and reports t

When the launcher path fails, preserve the failed spec and fallback reason, then pivot cleanly to direct `codex exec` rather than silently switching behavior.

If `/sub` resolves to `team mode` on Linux or macOS and no PowerShell host is available, treat that as a launcher-path failure for bounded one-off work and pivot to direct `codex exec` instead of blocking the request.

If the parent pivots to direct `codex exec`, it should preserve the intended worker settings explicitly:

- pass `-m` when the model choice matters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ param(

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$script:IsWindowsPlatform = [System.IO.Path]::DirectorySeparatorChar -eq '\'

function Get-OptionalProperty {
param(
Expand Down Expand Up @@ -170,6 +171,55 @@ function Resolve-CommandPath {
return $normalized
}

function Resolve-PowerShellHost {
$currentProcessPath = $null
try {
$currentProcessPath = Get-Process -Id $PID -ErrorAction Stop | Select-Object -ExpandProperty Path -First 1
} catch {
$currentProcessPath = $null
}

if (-not [string]::IsNullOrWhiteSpace($currentProcessPath)) {
return [string]$currentProcessPath
}

$candidates = if ($script:IsWindowsPlatform) {
@("powershell.exe", "pwsh.exe", "pwsh", "powershell")
} else {
@("pwsh", "powershell")
Comment on lines +183 to +189
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve-PowerShellHost returns the current process executable path whenever Get-Process exposes it. If the queue runner is hosted by something other than pwsh/powershell (e.g., a GUI/editor host), the later Start-Process call will try to launch that executable with -File/-NoProfile flags and can fail. Recommend validating the host executable (pwsh/powershell) before returning it, otherwise prefer $PSHOME-based resolution or the existing candidate list.

Suggested change
return [string]$currentProcessPath
}
$candidates = if ($script:IsWindowsPlatform) {
@("powershell.exe", "pwsh.exe", "pwsh", "powershell")
} else {
@("pwsh", "powershell")
$fileName = [System.IO.Path]::GetFileNameWithoutExtension($currentProcessPath)
if ($fileName -and ($fileName.Equals("pwsh", [System.StringComparison]::OrdinalIgnoreCase) -or $fileName.Equals("powershell", [System.StringComparison]::OrdinalIgnoreCase))) {
return [string]$currentProcessPath
}
}
$candidates = @()
if ($PSHOME -and (Test-Path $PSHOME)) {
if ($script:IsWindowsPlatform) {
$candidates += @(
(Join-Path $PSHOME "pwsh.exe"),
(Join-Path $PSHOME "powershell.exe")
)
} else {
$candidates += @(
(Join-Path $PSHOME "pwsh"),
(Join-Path $PSHOME "powershell")
)
}
}
if ($script:IsWindowsPlatform) {
$candidates += @("powershell.exe", "pwsh.exe", "pwsh", "powershell")
} else {
$candidates += @("pwsh", "powershell")

Copilot uses AI. Check for mistakes.
}

foreach ($candidate in $candidates) {
try {
return Resolve-CommandPath -Executable $candidate
} catch {
continue
}
}

throw "Unable to find a PowerShell host. Install PowerShell (`pwsh` on Linux/macOS) or run from PowerShell on Windows."
}

function Get-PowerShellHostArguments {
param([string[]]$AdditionalArguments)

$arguments = New-Object System.Collections.Generic.List[string]
$arguments.Add("-NoProfile")

if ($script:IsWindowsPlatform) {
$arguments.Add("-ExecutionPolicy")
$arguments.Add("Bypass")
}

foreach ($entry in @($AdditionalArguments)) {
if ($null -ne $entry) {
$arguments.Add([string]$entry)
}
}

return [string[]]$arguments.ToArray()
}

function Get-SafePathSegment {
param(
[string]$Value,
Expand Down Expand Up @@ -1282,10 +1332,8 @@ function Start-IssueProcess {
$stdoutPath = Resolve-AbsolutePath -Path (Join-Path $logRoot "launcher.stdout.log") -BaseDirectory $logRoot -AllowMissing
$stderrPath = Resolve-AbsolutePath -Path (Join-Path $logRoot "launcher.stderr.log") -BaseDirectory $logRoot -AllowMissing

$args = @(
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
$powerShellHost = Resolve-PowerShellHost
$args = Get-PowerShellHostArguments -AdditionalArguments @(
"-File",
$QueueConfig.launcher_script,
"-SpecPath",
Expand All @@ -1296,7 +1344,7 @@ function Start-IssueProcess {
)

$process = Start-Process `
-FilePath "powershell.exe" `
-FilePath $powerShellHost `
-ArgumentList (Join-ArgLine -Items $args) `
-WorkingDirectory $QueueConfig.config_directory `
-RedirectStandardOutput $stdoutPath `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ param(

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$script:IsWindowsPlatform = [System.IO.Path]::DirectorySeparatorChar -eq '\'

$FallbackPrincipalEngineerDirective = @'
You are a principal software engineer, reviewer, and production architect whose goal is to turn every request into code that improves code health, not merely code that runs once. For each task, infer the real objective, runtime environment, interfaces, invariants, data model, trust boundaries, failure modes, concurrency risks, performance limits, rollback needs, then choose the smallest design that fully solves problem without decorative abstraction. Favor clear names, explicit control flow, narrow public surfaces, cohesive modules, visible state, boundary validation, safe defaults, precise errors, and behavior that stays predictable under retries, timeouts, malformed input, partial failure, and load. Follow local conventions first, use idiomatic tooling, prefer the standard library and proven dependencies, preserve behavior during refactoring, and separate structural cleanup from behavior change when practical. Build security, observability, and operability into the code through least privilege, secret-safe handling, logs, metrics, traces, health signals, and graceful failure. Write tests around observable behavior, edge cases, regressions, and critical contracts. When details are missing, state the smallest safe assumption and continue. Before finalizing, run a silent senior review for correctness, simplicity, maintainability, security, performance, and rollback safety, then present brief assumptions and design intent, complete code, tests, and concise verification notes.
Expand Down Expand Up @@ -155,6 +156,55 @@ function Resolve-CommandPath {
return $normalized
}

function Resolve-PowerShellHost {
$currentProcessPath = $null
try {
$currentProcessPath = Get-Process -Id $PID -ErrorAction Stop | Select-Object -ExpandProperty Path -First 1
} catch {
$currentProcessPath = $null
}

if (-not [string]::IsNullOrWhiteSpace($currentProcessPath)) {
return [string]$currentProcessPath
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve-PowerShellHost returns the current process executable path unconditionally when it can read it. If this script is run under a non-CLI host process (e.g., powershell_ise.exe or another host executable), Start-Process will later invoke that host with CLI-only flags like -EncodedCommand/-NoProfile, which can fail. Consider only accepting the current process path when the process name/path indicates pwsh/powershell, otherwise fall back to $PSHOME\pwsh(.exe)/powershell.exe or the candidate resolution list.

Suggested change
return [string]$currentProcessPath
$exeName = [System.IO.Path]::GetFileNameWithoutExtension($currentProcessPath)
if ($exeName -and @('pwsh', 'powershell') -contains $exeName.ToLowerInvariant()) {
return [string]$currentProcessPath
}

Copilot uses AI. Check for mistakes.
}

$candidates = if ($script:IsWindowsPlatform) {
@("powershell.exe", "pwsh.exe", "pwsh", "powershell")
} else {
@("pwsh", "powershell")
}

foreach ($candidate in $candidates) {
try {
return Resolve-CommandPath -Executable $candidate
} catch {
continue
}
}

throw "Unable to find a PowerShell host. Install PowerShell (`pwsh` on Linux/macOS) or run from PowerShell on Windows."
}

function Get-PowerShellHostArguments {
param([string[]]$AdditionalArguments)

$arguments = New-Object System.Collections.Generic.List[string]
$arguments.Add("-NoProfile")

if ($script:IsWindowsPlatform) {
$arguments.Add("-ExecutionPolicy")
$arguments.Add("Bypass")
}

foreach ($entry in @($AdditionalArguments)) {
if ($null -ne $entry) {
$arguments.Add([string]$entry)
}
}

return [string[]]$arguments.ToArray()
}

function Quote-Arg {
param([string]$Value)

Expand Down Expand Up @@ -796,9 +846,11 @@ function Invoke-PowerShellHook {

$encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Command))
try {
$powerShellHost = Resolve-PowerShellHost
$powerShellArgs = Get-PowerShellHostArguments -AdditionalArguments @("-EncodedCommand", $encodedCommand)
$process = Start-Process `
-FilePath "powershell.exe" `
-ArgumentList "-NoProfile -ExecutionPolicy Bypass -EncodedCommand $encodedCommand" `
-FilePath $powerShellHost `
-ArgumentList (Join-ArgLine -Items $powerShellArgs) `
-WorkingDirectory $RunCwd `
-RedirectStandardOutput $StdoutPath `
-RedirectStandardError $StderrPath `
Expand Down Expand Up @@ -995,7 +1047,7 @@ function Find-SessionLogPath {
return $null
}

$sessionsRoot = Join-Path $HOME ".codex\sessions"
$sessionsRoot = Join-Path (Join-Path $HOME ".codex") "sessions"
if (-not (Test-Path -LiteralPath $sessionsRoot)) {
return $null
}
Expand Down
Loading