Skip to content

feat(config): support file:// URI for task, matching system_prompt pattern #163

@jduncan-rva

Description

@jduncan-rva

Summary

Extend the task field in api.ScionConfig (and the positional arg to scion start) to support a file:// URI, resolved on the host before container launch, matching the existing system_prompt: file:// pattern already in the inline-config system at cmd/common.go:132-141. The implementation is mostly infrastructure-reuse: <agentDir>/prompt.md is already written per-agent at pkg/agent/provision.go:284-288, but it's currently not bind-mounted — the content is extracted back out and passed inline as a CLI arg. This issue proposes mounting the file and passing a shell-safe container-side path to the harness.

Related: #162 (independent bug-fix for shell-escape correctness in the current inline path). This issue is additive — a cleaner path for structured prompts — rather than a fix for the escaping bug. Both stand on their own.

Motivation

Task prompts are frequently authored as markdown — code fences, backticks, headings, checklists, price/ID references ($), file globs. Passing markdown through the current tmux command string even after #162 still commits every consumer to perfect shell-quoting of every character Scion might ever consider special. File indirection sidesteps the problem entirely: the content never travels through a shell.

The file:// URI approach is preferred over a new --prompt-file flag because:

  1. Precedent already exists. cmd/common.go:132-141 resolves system_prompt: file://prompt.md host-side; the same mechanism applied to task keeps the CLI and inline-config surfaces symmetric.
  2. No new CLI flag. scion start <agent> "file:///path/to/task.md" reuses the existing positional arg.
  3. Composable with inline-config. A grove-level scion-agent.yaml can declare task: file://prompt.md just like it already can for system_prompt.

Current state

pkg/agent/provision.go:284-288 writes the task string to <agentDir>/prompt.md on the host at provision time. pkg/agent/run.go:147-163 reads it back if the CLI task is empty, or overwrites it with the current task if provided. But the file itself is never mounted into the container — each harness receives the task as an inline CLI arg:

  • pkg/harness/claude_code.go:83-85 — bare positional arg
  • pkg/harness/gemini_cli.go:82-84--prompt-interactive <task>
  • pkg/harness/codex.go:68-70 — positional arg
  • pkg/harness/opencode.go:67-70--prompt <task>

The mount primitives needed to change this already exist:

  • pkg/runtime/common.go:607-682writeFileSecrets() — stages host file and emits -v host:container:ro mount spec
  • pkg/runtime/common.go:560-602applyResolvedAuth()api.FileMapping{SourcePath, ContainerPath} → mount arg
  • registerMount flow in buildCommonRunArgs — the spot a prompt mount would hook into

Proposed design

Resolution:

  1. cmd/common.go — when the task string (CLI positional arg or scion-agent.yaml task: field) starts with file://, resolve the path on the host (tilde-expand; relative paths resolved against grove root).
  2. pkg/agent/run.go — copy/symlink the resolved file into <agentDir>/prompt.md if not already there (i.e., unify with the existing prompt.md path rather than introducing a second location). Fail loudly if the source file doesn't exist.
  3. pkg/runtime/common.go / buildCommonRunArgs — append a -v <agentDir>/prompt.md:/home/scion/prompt.md:ro mount to the run args (container path can be any stable location; /home/scion/prompt.md matches the "home-adjacent" convention used by other mounts).
  4. Each harness's GetCommand — when the task was file-resolved, pass the container-side path as the task arg instead of the file contents. Concretely: the harnesses use whatever flag/positional they already use today, but the value is /home/scion/prompt.md (shell-safe literal) rather than the file's contents.

Step 4 is where the design question lives: do harnesses accept a file path as their task arg?

  • Claude Code: positional arg is a prompt string, not a file path. The agent would need to be told "the prompt is in this file" explicitly. Simplest path: pass a short literal prompt like "Read /home/scion/prompt.md and execute the task described there." This is what makes the whole scheme shell-safe — the literal prompt has no metacharacters.
  • Gemini CLI: --prompt-interactive takes a string; same treatment.
  • Codex / OpenCode: same.

The short literal prompt is boilerplate per-harness but deterministic and auditable. None of the harnesses have a native --prompt-file flag today (confirmed against current pkg/harness/*.go), so this Scion-level indirection is the right layer.

Alternative considered: stdin-piping the prompt into the harness. Rejected because it requires restructuring the tmux session launch (interactive harnesses read stdin from the tmux PTY, not from a pipe) and because it doesn't compose with the sciontool hook lifecycle.

Acceptance

  • task: in scion-agent.yaml and the positional arg to scion start accept file:// URIs with the same resolution semantics as system_prompt: file://.
  • Resolved file lands at a stable container-side path (proposed: /home/scion/prompt.md) via the existing registerMount / api.FileMapping plumbing.
  • Harness GetCommand implementations pass a short literal prompt instructing the harness to read the container-side file, for all supported harnesses (Claude Code, Gemini CLI, Codex, OpenCode).
  • Integration test: scion start with a task: file://... pointing at a markdown file containing backticks, $, globs, parens, and multi-line content lands the exact file contents at the container-side mount point, byte-for-byte. No shell interpretation.
  • Missing source file fails loudly at agent start (task: file://... not found at <resolved-path>), exit non-zero.
  • Existing inline task strings (no file:// prefix) work unchanged — this is opt-in.

Out of scope

  • Changing the default behavior (positional arg without file:// keeps its current semantics).
  • Per-harness native --prompt-file flags (not currently available in the target harnesses; Scion-level indirection is sufficient).
  • Templated paths in the URI (file://{{ grove }}/task.md) — ship with literal paths first.
  • Hot-reload of the task file into a running agent.

Context

Branch main, commit 22dfe46a. No prior issue or PR addresses prompt-file indirection. The system_prompt: file:// precedent at cmd/common.go:132-141 is the closest idiom in the codebase. #162 is independent — even without file:// support, the escaping bug should be fixed on the inline path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions