clikae Docs

Proposal — Issue #24: simplify clikae burn (drop the per-engine flag wrangling)

Status: ✅ SHIPPED in v0.6.0 (the full design — --prompt / --prompt-file / --add-dir + the per-engine adapter_burn_flags hook). This file is now an archived reference for the rationale; the live behaviour is authoritative. What actually shipped:

  • Convenience surface: clikae burn <engine> <tank> --artifact <path> (--prompt-file <f> | --prompt <str>) with --add-dir defaulting to the artifact's parent (lib/commands/burn.sh, cmd_burn / _burn_compose).
  • Per-engine write hook adapter_burn_flags on claude + codex (NUL-separated argv, so a multi-line prompt survives as one item), stubbed in _template.sh, added to the adapter-loader leak-guard.
  • Cross-engine --to reroute regenerates the flags for the new engine (the soundness fix in §2/§3c).
  • Docs: docs/grammar.md §3.3 documents both the easy and power-user forms; tests in tests/bats/burn.bats (incl. the direct adapter_burn_flags unit tests and the cross-engine-regenerate case).

The raw -- <cmd…> power-user form is unchanged (fully additive, as designed).

Original draft preserved verbatim below.


Status: draft / design Author: design note (for review) Touches: lib/commands/burn.sh, lib/adapters/*.sh, lib/core/adapter_loader.sh, tests/bats/burn.bats, docs/grammar.md, docs/usage.md Backward compatible: yes (additive — the current -- <cmd…> form keeps working unchanged)


1. Problem statement

clikae burn already nails its core bet (artifact verification + dry-reroute + cross-account offload). The remaining friction, surfaced directly in the dogfood writeup (docs/dogfood-burn-tugtile.md § friction 1), is that the caller must hand-assemble each engine's headless invocation by hand:

# claude — must remember -p + the skip-permissions flag + --add-dir
clikae burn claude L --to C --artifact out/core.test.cjs \
  -- -p "$(cat out/promptA.txt)" --dangerously-skip-permissions --add-dir /…/tugtile

# codex — a completely different flag shape
clikae burn codex M --artifact /tmp/out.md \
  -- exec -C /tmp -s workspace-write "read /tmp/in.txt, write /tmp/out.md"

To burn anything the first time you have to go read claude --help / codex --help, get the headless flags right, and quote a multi-line prompt inside --. Get it wrong and you fire a blank cartridge — the engine runs, exits 0, and writes nothing, which is exactly the failure mode burn was built to detect but which the user just hand-wrote into existence.

The task burn is built for is almost always the same shape: "here is a prompt, here is the file it should produce, run it headless with write access to this directory." That intent should not require the user to know each CLI's flag dialect.

2. Current pain points

Pain Where it bites
Per-engine headless flags are inconsistent claude: -p … --dangerously-skip-permissions --add-dir <dir>; codex: exec -C <dir> -s workspace-write "<prompt>". No shared mental model.
Prompt goes inside --, so it's quoting hell The dogfood worked around it with -p "$(cat out/promptA.txt)". The prompt is the content of the task, yet it's tangled with engine plumbing.
Knowledge lives in the user's head, not the tool clikae already owns an adapter per engine — it knows what "claude" is — but burn makes the human re-supply that knowledge every call.
Cross-engine --to reroute is unsound by construction Today the SAME literal argv runs under the next engine. claude's -p … flags are nonsense to codex, so a cross-engine dry-reroute of a hand-written command silently misfires. A per-engine flag hook also fixes this.
New engines can't participate Any future adapter (agy excepted, see §6) would need the user to learn yet another flag dialect.

3. Design

Add a high-level "write a file from a prompt" path to burn, and push the per-engine flag knowledge into the adapters where it belongs.

3a. New burn options (the convenience surface)

clikae burn <engine> <tank> --artifact <path> --prompt-file <f> [--add-dir <dir>] [existing flags…]
clikae burn <engine> <tank> --artifact <path> --prompt      <str> [--add-dir <dir>] [existing flags…]
  • --prompt-file <f> — read the task prompt from a file (no quoting hell; pairs naturally with the dogfood's own out/promptA.txt habit).
  • --prompt <str> — inline prompt, for one-liners.
  • --add-dir <dir> — the directory the engine may write in. Defaults to the artifact's parent directory (so the engine can always at least write the artifact you asked for). Repeatable.
  • When --prompt/--prompt-file is used, -- is optional; if both are present, the post--- argv is appended verbatim after the engine's generated flags (escape hatch for extra per-engine args).

So the dogfood command collapses to:

clikae burn claude L --to C --artifact out/core.test.cjs \
  --prompt-file out/promptA.txt --add-dir /…/tugtile

…and the very same intent works on codex with zero flag changes:

clikae burn codex M --artifact /tmp/out.md --prompt-file task.txt --add-dir /tmp

3b. The per-engine write hook: adapter_burn_flags

Each adapter self-reports how to run itself headless-with-write. New optional adapter hook:

# adapter_burn_flags <prompt> <add-dirs…>
#   Print, one argv item per line, the full engine argv (AFTER the binary) that
#   runs <prompt> HEADLESS with permission to WRITE in each <add-dir>. The prompt
#   is passed as data, never re-quoted by the caller. Return non-zero (print
#   nothing) if this engine has no headless-write mode (→ burn errors clearly).

Newline-per-item is the same convention already used by adapter_resume_args / adapter_flag_args, so mapfile/while read assembly is consistent across the codebase and prompts containing spaces survive intact.

claude:

adapter_burn_flags() {
  local prompt="$1"; shift
  printf -- '-p\n%s\n--dangerously-skip-permissions\n' "$prompt"
  local d; for d in "$@"; do printf -- '--add-dir\n%s\n' "$d"; done
}

codex:

adapter_burn_flags() {
  local prompt="$1"; shift
  printf 'exec\n'
  local d; for d in "$@"; do printf -- '-C\n%s\n' "$d"; done   # codex -C = working dir
  printf -- '-s\nworkspace-write\n%s\n' "$prompt"
}

_template.sh gets a documented stub so adapter authors know the contract.

3c. How burn uses it

When --prompt/--prompt-file is supplied, burn builds cmd from the hook instead of (or before) the post--- argv:

if [ -n "$prompt_src" ]; then
  declare -F adapter_burn_flags >/dev/null \
    || log_fail "$cli has no headless-write recipe (adapter defines no adapter_burn_flags). Use the explicit '-- <cmd…>' form."
  local -a gen=()
  while IFS= read -r line; do gen+=("$line"); done < <(adapter_burn_flags "$prompt" "${add_dirs[@]}")
  cmd=("${gen[@]}" "${cmd[@]}")   # generated flags first, any post-`--` argv appended verbatim
fi

Crucially this lives inside the reroute while loop's engine-resolution step (or is recomputed when cli changes on a cross-engine --to), so a cross-engine reroute regenerates the flags for the new engine — fixing pain point §2 (cross-engine reroute no longer ships claude flags to codex).

4. API sketch

clikae burn <engine> <tank> --artifact <path>
            ( --prompt-file <f> | --prompt <str> | -- <engine cmd…> )
            [--add-dir <dir>]…        # default: dirname(<artifact>); repeatable
            [--to <target>] [--timeout <s>] [--no-reroute]
            [--allow-active] [--fresh]

Resolution rules:

  • --prompt-file and --prompt are mutually exclusive (error if both).
  • If neither prompt option nor -- is given → error (today's "missing command" error, reworded to mention the prompt options).
  • If a prompt option and -- are both given → generated flags first, -- argv appended (advanced escape hatch).
  • --add-dir with no value defaults to dirname(--artifact).

Adapter contract:

adapter_burn_flags <prompt> [add-dir…]   # optional hook; newline-per-argv; non-zero ⇒ no headless-write mode

5. Backward compatibility

  • Fully additive. Every existing clikae burn … -- <cmd…> call is untouched: when no --prompt/--prompt-file is present, burn behaves exactly as today and adapter_burn_flags is never called.
  • Adapters without the new hook keep working with the explicit -- form; only the convenience path requires the hook, and its absence yields a clear, actionable error pointing back at -- <cmd…>.
  • No grammar/verb change (docs/grammar.md §"we do not invent cute verbs" stays satisfied — this adds options, not a command).
  • The hook name adapter_burn_flags is added to the unset-list in lib/core/adapter_loader.sh (lines ~164-170) so it can't leak across adapters loaded in one process (same discipline as the other optional hooks).

6. agy note

agy stays rejected by burn (global single-account — lib/commands/burn.sh already errors early, and docs/dogfood-burn-tugtile.md postscript explains why). It simply won't define adapter_burn_flags. The separate "let burn drive agy via serialize-and-restore" idea from that postscript is out of scope for #24 and tracked elsewhere.

7. Implementation sketch (files / functions)

  1. lib/commands/burn.sh

    • cmd_burn() arg loop: add --prompt, --prompt-file, --add-dir cases; locals prompt="", prompt_src="", add_dirs=().
    • After parsing: resolve --prompt-file → read into prompt (log_fail if unreadable); enforce mutual exclusion; default add_dirs to dirname "$artifact" when empty.
    • Relax the ${#cmd[@]} -ge 1 guard: require either a prompt source or a post--- cmd.
    • Inside the reroute loop, after the adapter is (re)loaded for the current cli, build cmd from adapter_burn_flags when prompt_src is set — so cross-engine --to regenerates correctly.
    • _burn_help(): document the three forms + --add-dir, with the dogfood one-liner as the headline example.
  2. lib/adapters/claude.sh — add adapter_burn_flags (§3b).

  3. lib/adapters/codex.sh — add adapter_burn_flags (§3b).

  4. lib/adapters/_template.sh — add the documented optional stub.

  5. lib/core/adapter_loader.sh — add adapter_burn_flags to the unset -f leak-guard list (~L167).

  6. docs/grammar.md + docs/usage.md — show the --prompt-file/--add-dir form as the recommended way to burn a write-task; keep -- <cmd…> documented as the power-user escape hatch.

8. Tests (bats)

Extend tests/bats/burn.bats (stubbed codex, no real engine — same pattern as today). The existing stub keys behaviour off argv; extend it to honour the generated exec … -C … -s workspace-write <prompt> shape (write the artifact when it sees the generated form).

New cases:

  1. --prompt-file builds the engine command via the hook and completes. burn a write-task with --prompt-file, assert artifact appears + "Done on codex/…", and that the user never typed any codex flags.
  2. --prompt inline equivalent produces the same outcome.
  3. --add-dir defaults to the artifact's parent when omitted (assert the generated argv carries -C <dirname(artifact)> — capture via a stub that echoes its argv to a side file).
  4. Mutually-exclusive prompt options error (--prompt + --prompt-file together → non-zero, message names both).
  5. No prompt and no -- errors with a message mentioning the prompt options.
  6. Missing adapter_burn_flags → clear error. Drive a synthetic adapter with no hook via --prompt; assert it fails pointing at the -- <cmd…> form (unit-level: source burn.sh, stub declare -F path, or use a throwaway adapter fixture).
  7. Cross-engine --to regenerates flags for the new engine (the soundness fix): start on a stub engine A whose tank is dry, --to a stub engine B, assert B is invoked with B's generated flags, not A's. (Two stub binaries on PATH, each recording its argv.)
  8. Direct adapter_burn_flags unit tests — source lib/adapters/claude.sh and codex.sh, call the hook, assert the exact newline-per-argv output (so the flag recipe is pinned and a CLI flag rename is caught here, not in the field). Mirrors the existing _burn_timeout_bin / _burn_next_same_engine direct-unit style.

Regression: keep all current -- <cmd…> tests green — they prove the additive path didn't disturb the legacy form.