Skip to content

[bug] Detached runs fail on native Windows (runner file URL and Pi npm shim) #139

Description

@yastman

Describe the bug

Detached/background runs fail on native Windows before an agent can complete. There are two sequential Windows-specific blockers in the current main implementation:

  1. runnerModulePath() converts import.meta.url to a drive-letter path, but taskflow-core/detached-runner later passes that value to dynamic import(). Node's ESM loader rejects C:\... as protocol c:.
  2. After preserving the runner as a file:// URL, the detached Node process falls back to spawn("pi", ...). A native npm installation exposes Pi through Windows shims (pi.cmd / pi.ps1), so the child fails with spawn pi ENOENT. Passing pi.cmd directly to spawn() without a shell produces EINVAL on Node 24.

To reproduce

On native Windows, install Pi and pi-taskflow@0.2.10, save a minimal flow, and start it detached:

{
  "name": "windows-detached-repro",
  "concurrency": 1,
  "phases": [
    {
      "id": "probe",
      "type": "agent",
      "agent": "scout",
      "task": "Return OK.",
      "final": true
    }
  ]
}

Invoke the Taskflow tool with:

{
  "action": "run",
  "name": "windows-detached-repro",
  "detach": true
}

Then inspect the run through /tf runs or its persisted JSON state.

Expected behavior

The detached runner loads the Pi subagent runner and starts the configured Pi child on native Windows.

Observed behavior

The unmodified package persists this terminal failure:

Runner module failed to load: 'C:\...\pi-taskflow\dist\runner.js'
Only URLs with a scheme in: file, data, and node are supported by the default ESM loader.
On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'

Changing runnerModulePath() locally to return import.meta.url exposes the second blocker:

spawn pi ENOENT

With both local changes applied—preserving the file:// runner URL and re-entering the installed Pi JS CLI via process.execPath on Windows—the same detached flow completes successfully. A three-phase read-only flow (script preflight, Pi worker, Pi review gate) also completed 3/3 in detached mode.

Environment

  • pi-taskflow version: 0.2.10
  • taskflow-core version: 0.2.10
  • Pi version: 0.84.2
  • Node version: 24.18.0
  • OS: native Windows 11

Additional context

Current main still contains both relevant paths:

return { command: "pi", args };

and:

export function runnerModulePath(): string {
  return fileURLToPath(import.meta.url);
}

Potential fixes:

  • Keep runnerModulePath() as a file:// URL, or normalize filesystem paths with pathToFileURL() immediately before dynamic import.

  • On Windows, resolve the actual installed Pi JS entry point and launch it with process.execPath, or serialize the initiating Pi CLI entry into the detached context. Avoid shell: true so arguments remain argv-safe.

  • Reproduces on a fresh detached run: yes

  • Foreground execution: works

  • Persisted detached state correctly records both failures; the issue is runner startup rather than state persistence.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions