Files
pad/cmd
xarmian fed4b60e65 feat(playbook): add pad playbook CLI (list/show/run) + endpoints (TASK-1382) (#520)
* feat(playbook): add pad playbook CLI (list/show/run) + endpoints (TASK-1382)

PLAN-1377 T5 — first-class invokable-procedure surface. Three HTTP
endpoints + three CLI subcommands, with a strict CLI arg parser and a
side-effect-free run path.

Endpoints

  GET  /workspaces/{ws}/playbooks         — metadata array, same
                                            projection as bootstrap
  GET  /workspaces/{ws}/playbooks/{ref}   — full item, resolved by
                                            invocation_slug | ref | slug
  POST /workspaces/{ws}/playbooks/{ref}/run — bind args, return body +
                                              bound + unbound. No
                                              execution; the agent
                                              owns step playback.

Resolution

  resolvePlaybook walks invocation_slug first (the /pad <slug>
  user-facing identifier), then falls back to ResolveItem (UUID / ref /
  slug). A stray TASK-5 hitting /playbooks/TASK-5 returns 404 instead
  of leaking a non-playbook into the surface.

Arg parsing

  ParsePlaybookCLIArgs implements the strict rules from PLAN-1377:
    - Required positional args first, in declared order.
    - Flag types: bareword presence sets true.
    - Other types: key=value form (number is parsed as float64,
      enum is validated against declared options).
  The server takes either pre-parsed args (MCP / programmatic callers)
  OR raw CLI tokens (CLI caller); merge logic prefers explicit args
  over raw_args. The CLI sends raw_args, so there is one parser
  implementation and no drift risk.

CLI

  pad playbook list                        — table or json
  pad playbook show <slug|ref> [--format]  — markdown / json
  pad playbook run <slug|ref> [args...]    — body + bound args

Client method

  cli.Client.{ListPlaybooks, ShowPlaybook, RunPlaybook(args,
  rawArgs)} — runtime callers pass args; CLI passes rawArgs.

Tests

  TestPlaybookList, TestPlaybookShowByInvocationSlug,
  TestPlaybookShowByRef, TestPlaybookShowRejectsNonPlaybook,
  TestPlaybookRunBindsArgs (with-args, missing-required, with-raw-args),
  TestParsePlaybookCLIArgsErrors, TestPlaybookListEmptyShape.

Parent: PLAN-1377.

* fix(playbook): tighten arg parsing per Codex round 1 (TASK-1382)

P2.1 — Positional binding now skips optional and flag-typed slots.
The PLAN-1377 contract says ONLY required args fill positional slots;
other typed args must be key=value. Without this, a spec with an
optional arg before a required one (e.g.
[merge-strategy?, target!]) bound the caller's bareword TASK-7 to
merge-strategy instead of target.

P2.2 — number coercion now uses strconv.ParseFloat with NaN/Inf
rejection. Sscanf(%g) was sloppy: it accepted '1abc' as 1 (partial
match) and accepted NaN/Inf, which json.Marshal then refused after
the handler had already written a 200 header.

P3 — empty-body run requests now decode cleanly. The decodeJSON
wrapper folds io.EOF into 'invalid JSON: EOF' so the previous
err.Error() != "EOF" check never fired. errors.Is(err, io.EOF) on
the unwrapped chain handles the wrapping correctly.

Tests: TestPlaybookRunAcceptsEmptyBody,
TestParsePlaybookCLIArgsOptionalNotPositional,
TestCoercePlaybookValueNumberRejectsBadInput.

Parent: PLAN-1377.
2026-05-12 18:29:11 -04:00
..