diff --git a/docs-ai/063-agent-workflows/000-plan.md b/docs-ai/063-agent-workflows/000-plan.md index d137f74b..e1a79bae 100644 --- a/docs-ai/063-agent-workflows/000-plan.md +++ b/docs-ai/063-agent-workflows/000-plan.md @@ -119,8 +119,9 @@ that advances one step at a time through existing terminal boundaries: never be misattributed. Tokens are never written into YAML, and the **generated command is the only spelling agents ever see**: one completion-command renderer produces the initial hint, every nudge, and every re-delivery (token always present; for verdict - steps a `--verdict VALUE -` form plus one executable command per allowed value in the - materialized instruction and in `prowl workflow status`); built-ins and examples say + steps one complete executable command per allowed value on every transport — typed + line, materialized instruction, and `prowl workflow status` — never a placeholder); + built-ins and examples say "finish with the generated completion command"; the validator warns when `text`/`instruction` spells out `prowl workflow done`. `expect` is valid only on `message` and `launch` (their target role delivers); native actions return typed @@ -131,8 +132,10 @@ that advances one step at a time through existing terminal boundaries: - `notify`/`close` → the existing bell pipeline and protected close path. **Data channels.** Inbound to an agent is always *file + short pointer*: long -`instruction` text is materialized to `run.dir/instructions/.md` and one line is -typed (or passed as the kickoff prompt); short `text` is typed verbatim (single line). +`instruction` text is materialized (one file per invocation, named by the run-global +invocation ordinal — the DSL spec §§5/8 are normative for run-directory layout) and one +line is typed (or passed as the kickoff prompt); short `text` is typed verbatim (single +line). Outbound is `prowl workflow done [--verdict v] -` (stdin): the caller pane identifies the run/role, the delivery token identifies the awaited step — the YAML itself carries nothing machine-specific. Transcript observation (`agents read`) and headless adapter capture are @@ -166,9 +169,10 @@ schedules cancellable grace deadlines on the injected clock; it never relies on event alone. **Data bus.** `/.prowl/workflow-runs//` holds `run.json`, `log.md`, -`instructions/`, `skills/` (materialized from the embedded skill registry only — `skill:` -ids are safe slugs that must resolve to a bundled skill), `outputs/.md` (every -delivery also kept as `..md`; latest wins in templates). +`instructions/` and `outputs/` (both versioned by the run-global invocation ordinal, latest +output view replaced atomically — layout normative in the DSL spec §8), `skills/` +(materialized from the embedded skill registry only — `skill:` ids are safe slugs that must +resolve to a bundled skill). Distribution is "the next instruction names the path"; Prowl never inlines one agent's output into another agent's input box, and every rendered line is re-validated as a single terminal line before injection (template values such as inputs or paths cannot @@ -318,9 +322,10 @@ to|save` remain (see Open questions for alias vs. removal). synchronous CLI call): a launch/provision failure after the artifacts are written ends the run as `failed`, keeps artifacts and log, and returns `HANDOFF_FAILED` exactly as the current handler does. The adapter awaits run completion synchronously (no agent wait is - involved once the brief is supplied) and is covered by per-field socket parity tests: - no-agent source, `--no-brief`, `--no-launch`, profile lookup failure, provisioning - failure, launch failure. + involved once the brief is supplied) and is covered by per-field socket parity tests + (the DSL spec §11 list is normative: no-agent source, `--no-brief`, `--no-launch`, + omitted brief choice, empty stdin, invalid sections, action/transition failure, profile + lookup failure, provisioning failure, launch failure). - `skills/prowl-workflows/SKILL.md`: how to author and run workflows; `prowl workflow schema` prints the machine-readable reference. @@ -350,9 +355,11 @@ Shapes are intentionally close to what exists so the runner and the CLI share on additively (`resource: pane`, `anchor`, `direction`, optional `launch {profile_id, profile_name, agent}`). `prowl profiles list` is a read-only snapshot of enabled/disabled profiles with availability (`prowl.cli.profiles.v1`). `prowl agents wait --until - idle|done|blocked|changed --timeout 1…3600` subscribes to the per-surface - `ActiveAgentEntry` events (immediate return when already satisfied; `WAIT_TIMEOUT` - otherwise) and returns `{status, raw_state, waited_ms}` (`prowl.cli.agents.wait.v1`). + idle|done|blocked|changed --timeout 1…3600` consumes the typed `ObservedAgentState` + observer described above (snapshot first → immediate return when already satisfied; + `WAIT_TIMEOUT` on expiry; `removed` / `surfaceClosed` → error `AGENT_GONE` with the last + known status in the error payload) and returns `{status, raw_state, waited_ms}` + (`prowl.cli.agents.wait.v1`). - **Tests**: terminal-layer coverage extends `supacodeTests/WorktreeTerminalStateAgentProfileTests.swift` (anchored split, placement override, background tab, returned identity, provisioning failure); planner intent rendering per adapter in `supacodeTests/AgentProfileTests.swift` @@ -490,6 +497,12 @@ built-ins land, handoff migrated). declared; optional fixes (`UNSAFE_PATH` listed, `tN` in the source grammar, concepts table and binding text aligned with the DSL, `notify` fallback without a `current` role, legacy parity list includes the preflight cases). +- **Review round 5 (2026-08-22; verified before adopting)** — run-directory specifics in + the plan now defer to the DSL spec (§§5/8 normative); a run-global *invocation* ordinal + is minted on entry to every `message`/`launch` execution (artifact naming for + non-waiting steps too), with *activation* = waiting invocation; `--role r=` in + the synopsis; `agents wait` wording aligned with the `ObservedAgentState` observer and + `AGENT_GONE` payload; a compact V1 action schema table added to the DSL. ## Open questions diff --git a/docs-ai/063-agent-workflows/dsl-spec.md b/docs-ai/063-agent-workflows/dsl-spec.md index 46269cf1..7af9dbdb 100644 --- a/docs-ai/063-agent-workflows/dsl-spec.md +++ b/docs-ai/063-agent-workflows/dsl-spec.md @@ -165,6 +165,15 @@ and every re-delivery after Relaunch, so no path ever shows a token-less or verdict-less command. Authors never spell the command (the validator warns when `text`/`instruction` contains `prowl workflow done`). +**V1 native action schemas** (normative summary; `prowl workflow schema` prints the full +JSON Schema): + +| Action | `with` inputs | Outputs | +| --- | --- | --- | +| `handoff.transition` | `briefing` (path, optional), `from` (role, required), `to` (role, required), `note` (string, optional) | `kickoff_prompt` (string), `artifact_path` (path), `has_briefing` (bool) | +| `handoff.checkpoint` | `briefing` (path, required), `note` (string, optional) | `artifact_path` (path) | +| `git.context` | `root` (path, optional; default worktree) | `path` (path to the generated markdown summary), `branch` (string) | + ## 5. `expect` ```yaml @@ -177,20 +186,21 @@ expect: on_timeout: attention # only with `timeout`: attention (default) | skip | cancel ``` -- **Activation identity.** Each time a step starts waiting — once for a plain step, once - per iteration inside `repeat` — the runner creates a new *activation* - `(run id, step id, activation ordinal, delivery role)` and mints a fresh delivery token - for it; the previous activation of the same step (if any) is terminal. The **activation - ordinal is run-global and monotonic** (1, 2, 3, … across all steps and iterations), so it - alone identifies an activation within a run. Exactly one successful `done` is accepted - per activation, identified by its token; a later, stale, or token-less delivery gets - `STEP_NOT_EXPECTING` / `TOKEN_REQUIRED` / `TOKEN_INVALID`. Skip / Cancel / Relaunch - revoke the *current* activation's token (Relaunch then mints a new activation). Every - delivery is persisted as `outputs/..md` (collision-free by construction, - even when several steps produce the same output name); `outputs/.md` is the - "latest" view, replaced atomically (temp file + rename); materialized instructions are - versioned the same way (`instructions/..md`), and `run.json` records the - activation → step / iteration / output-file mapping. +- **Invocation and activation identity.** Every execution of a `message` or `launch` step + — once for a plain step, once per iteration inside `repeat`, again after Relaunch — + mints a run-global, monotonic **invocation ordinal** (1, 2, 3, … across all steps and + iterations) on entry, whether or not the step waits; it names the step's artifacts + (`instructions/..md`). When the step has an `expect`, the same invocation + is also its *activation* `(run id, step id, ordinal, delivery role)`: the runner mints a + fresh delivery token for it, and the previous activation of the same step (if any) is + terminal. Exactly one successful `done` is accepted per activation, identified by its + token; a later, stale, or token-less delivery gets `STEP_NOT_EXPECTING` / + `TOKEN_REQUIRED` / `TOKEN_INVALID`. Skip / Cancel / Relaunch revoke the *current* + activation's token (Relaunch then mints a new invocation/activation). Every delivery is + persisted as `outputs/..md` (collision-free by construction, even when + several steps produce the same output name); `outputs/.md` is the "latest" view, + replaced atomically (temp file + rename); `run.json` records the invocation → step / + iteration / activation / file mapping. - Output bodies are capped (default 1 MiB, hard max 4 MiB → `OUTPUT_TOO_LARGE`). - **Skip rule.** Skipping an `expect` (panel Skip or `on_timeout: skip`) marks its output missing. If any later step's template references that output (or the `until` of an @@ -245,7 +255,7 @@ appends the rendered command itself); `on_timeout: skip` on an output referenced run.json # state snapshot: workflow id/version, frozen role bindings (profile UUID/name, pane ids), # step states, timestamps; no env values, no extra arguments, no credentials log.md # human-readable, append-only - instructions/..md # materialized `instruction` / `prompt` text, one per activation (run-global ordinal) + instructions/..md # materialized `instruction` / `prompt` text, one per invocation (run-global ordinal, §5) skills//SKILL.md # materialized bundled skills outputs/.md # latest validated delivery (atomically replaced); every delivery is also kept as ..md ``` @@ -269,7 +279,7 @@ output body is excluded from that claim. ```bash prowl workflow list [--json] # sources, enabled, validation status -prowl workflow run [source] [--role r=]... [--input k=v]... [--json] +prowl workflow run [source] [--role r=]... [--input k=v]... [--json] # grammar is source-specific, see below # [source]: 060 GenericTarget (pN | tN | UUID | worktree ref); omitted → caller pane # when the workflow has a `current` role (SOURCE_REQUIRED outside a pane), a # worktree reference otherwise @@ -334,7 +344,7 @@ changed` was requested. | Advance | A step completes when its `expect` is satisfied (or it has none and its effect succeeded). `repeat` evaluates `until` before entry and after each iteration; `max` reached with `until` still false ends the run as `max_rounds_reached`. | | Message delivery | Injection is synchronous (`insertCommittedText` + submit); Prowl keeps no queue — a `working` agent holds the line in its own input queue, and the panel says so. A `message` step advances only after a successful injection; a `blocked` role, a missing surface, or a failed injection leaves the step active in `needsAttention` (Retry / Skip / Cancel). At most one pending injection per role; Cancel / Skip / Relaunch drop it. | | Binding scope | As defined in §3 (four-tuple key with the canonical role-requirements digest); §3 is normative. | -| Activation | Defined in §5: every wait is a new activation with its own token; one delivery per activation; revocation is per activation; outputs are versioned by activation ordinal. | +| Invocation / activation | Defined in §5: every `message`/`launch` execution mints a run-global invocation ordinal (artifact naming); a waiting invocation is an activation with its own token; one delivery per activation; revocation is per activation; outputs and instructions are versioned by ordinal. | | Rendered-text boundary | Every string that reaches `insertCommittedText` + submit — rendered `text`, pointer lines, completion commands, nudges — is validated after template substitution: no line terminators (`\n`, `\r`, U+2028/2029) and no C0/C1 control characters; violations stop the step with `RENDERED_TEXT_INVALID` (`needsAttention`, never a partial injection). String inputs and `--input` values are validated the same way at start; a worktree path that cannot be rendered on one line is rejected at start (`UNSAFE_PATH`). Multi-line content always goes through `instruction` / materialized files. | | Watchdog | Driven by the periodic detection events (`agentEntryChanged` / `agentEntryRemoved`, forwarded to the runner by `AppFeature`'s single terminal-event subscription — `WorktreeTerminalManager.eventStream()` is single-consumer) plus an injected clock. When a step starts waiting the watchdog reads the role's current state first and schedules cancellable grace deadlines; it never depends on a later event alone. Every trigger has a grace period because detection is heuristic and a wrong guess must be harmless. Awaited role `blocked` ≥ `blocked_grace` (default 30 s) → `needsAttention` (Focus pane / Cancel). Awaited role `idle`/`done` ≥ `idle_grace` (default 3 min) without `done` → one automatic nudge (`[Prowl] When your work for this step is fully complete, finish with: `, harmless if the agent is still working because the runtime merely queues the line), then `needsAttention` after another `idle_grace` (Nudge again / Keep waiting / Skip / Cancel). Awaited role's agent process gone → `needsAttention` (Relaunch role / Skip / Cancel). `working` never triggers anything. Grace values are global settings. | | `needsAttention` | A UI state (orange status slot + notification), never a deadline: a late `done` is still accepted; only an explicit Skip marks the output missing and rejects later deliveries. |