Files
small-scripts/specs/bg-model-call.spec.md
Paul O'Reilly f50561ea78 feat(mm-worktree-task): run from any planner-backed repo — cp_cli dir + context resolution (--context, MM_WORKTREE_TASK_CP_*), empty-docs guard, tests + spec
cp_cli lives in agent-runtimes; previously every planner call ran 'python3 -m lib.cp_cli' from the current repo with no --context, so the script only worked inside agent-runtimes. The context now resolves from ~/.cpconfig by matching project_id to the repo directory name. Verified live from hatchmate (item + parent + documents fetched via the hatchmate context).

Claude-Session: https://claude.ai/code/session_01JgJbWzVJioVznVTFyQFEfx
2026-09-11 16:27:08 +12:00

159 lines
7.2 KiB
Markdown

# bg-model-call
## Purpose
Submit a prompt to a third-party model via the agent-runtimes control plane, wait for completion, and write the result to a file — enabling the `ScheduleWakeup` polling pattern for second-opinion consultations without blocking the main Claude Code session.
## Usage
```
bg-model-call [OPTIONS]
```
### Required flags
| Flag | Description |
|------|-------------|
| `--model <name>` | Model alias: `minimax`, `sonnet`, `haiku`, `opus` |
| `--prompt <file>` | Path to a file containing the prompt text |
| `--output <file>` | Path where the result will be written |
### Optional flags
| Flag | Default | Description |
|------|---------|-------------|
| `--template <name>` | (derived from `--model`) | Override the task template name (without `.yaml`). Takes precedence over the `--model` → template mapping. |
| `--project <id>` | `bg-model-call` | Project ID sent to the CP, used for task grouping in agent-monitor |
| `--url <cp-url>` | `$CP_URL`, then `http://localhost:8100` | Control plane base URL |
| `--task-name <name>` | `bg-model-call-<model>` | Short task name shown in agent-monitor |
| `--timeout <seconds>` | `1800` | Max seconds to wait for the task before declaring it failed |
| `--poll-interval <seconds>` | `10` | Seconds between CP state polls |
| `--dryrun`, `-n` | — | Resolve all values and print what would be submitted; do not call the CP or write any files |
| `--help`, `-h` | — | Show usage information and exit 0 |
## Model-to-Template Mapping
A constant defined at the top of the script:
| Model alias | Template name |
|-------------|---------------|
| `minimax` | `minimax-planning` |
| `sonnet` | `sonnet-code-repo` |
| `haiku` | `haiku-code-repo` |
| `opus` | `opus-code-repo` |
The `--template` flag overrides this mapping entirely.
## Behaviour
1. Parse and validate all flags. Exit 1 with an error message if required flags are missing or values are invalid.
2. Resolve the CP URL: `--url` flag → `$CP_URL` env var → `http://localhost:8100`.
3. Resolve the template name: `--template` flag if given, otherwise look up `--model` in the mapping table. If the model alias is not in the table, exit 1.
4. Read the prompt text from the `--prompt` file. If the file is not readable, exit 1.
5. Locate the `dispatch-task` CLI:
- Check if `dispatch-task` is on PATH.
- Otherwise try `<script-dir>/../../../projects/agent-runtimes/scripts/dispatch-task` as a fallback (for dev environments).
- If neither is found, fall back to direct `curl` POST to `$CP_URL/tasks`.
6. Submit the task to the CP via the resolved `dispatch-task` (or curl fallback):
- `--name <task-name>`
- `--project-id <project>`
- `--prompt-file <prompt-file>`
- `--template <template>`
- `--cp-url <cp-url>`
- `--format json` (to capture the task_id from stdout)
- The `dispatch-task` tool uses `CP_AUTH_TOKEN` from the environment if present.
7. Extract the `task_id` from the submission response.
8. Write a `.bg-model-call-<task-id>` tracking file in the same directory as `--output` (content: task ID, model, template, timestamp). This avoids collisions between concurrent calls.
9. Poll the CP using `wait-for-tasks` (or the `dispatch-task --wait` pattern):
- Locate `wait-for-tasks` using the same PATH + fallback logic as `dispatch-task`.
- If not found, poll manually via `curl GET $CP_URL/tasks/<task-id>` in a sleep loop.
- Poll every `--poll-interval` seconds up to `--timeout` seconds total.
10. Once the task reaches a terminal state:
- **Succeeded:** Fetch logs via `GET $CP_URL/tasks/<task-id>/logs`. Write the log content to `--output`. Write `<output>.done` sentinel (content: `task_id=<id>` + newline + `completed_at=<timestamp>`). Remove the tracking file.
- **Failed / timed_out / cancelled:** Write `<output>.failed` sentinel (content: `task_id=<id>`, `state=<state>`, `error=<detail if available>`). Remove the tracking file. Exit 1.
11. Exit 0 on success.
## Output Files
| File | When written | Content |
|------|-------------|---------|
| `<output>` | On success | Raw text from `GET /tasks/<id>/logs` |
| `<output>.done` | On success | `task_id=<id>`, `completed_at=<timestamp>` |
| `<output>.failed` | On failure/timeout | `task_id=<id>`, `state=<state>`, `error=<detail>` |
| `.bg-model-call-<task-id>` | After submission | `task_id=<id>`, `model=<alias>`, `template=<name>`, `submitted_at=<timestamp>` |
The `.done` sentinel is the signal the caller polls for. The caller reads `<output>` once `.done` appears.
## Dryrun Behaviour
When `--dryrun` or `-n` is passed, the script:
1. Resolves all values (CP URL, template, task name, project ID).
2. Reads (and validates) the prompt file.
3. Prints a summary of what would be submitted — no CP calls, no file writes.
Example output:
```
[dryrun] bg-model-call — no changes will be made
CP URL: http://localhost:8100
Model: minimax
Template: minimax-planning
Task name: bg-model-call-minimax
Project ID: bg-model-call
Prompt file: /tmp/my-prompt.txt
Prompt size: 142 bytes
Output file: /tmp/result.txt
Sentinel: /tmp/result.txt.done
dispatch-task: /home/user/dev/claude/projects/agent-runtimes/scripts/dispatch-task
```
## Edge Cases
| Case | Handling |
|------|----------|
| `--model` missing and `--template` missing | Error: `--model is required (or use --template to specify a template directly)`, exit 1 |
| `--prompt` missing | Error: `--prompt is required`, exit 1 |
| `--output` missing | Error: `--output is required`, exit 1 |
| `--model` not in mapping table | Error: `Unknown model alias '<name>'. Valid values: minimax, sonnet, haiku, opus`, exit 1 |
| Prompt file not readable | Error: `Cannot read prompt file: <path>`, exit 1 |
| `--timeout` not a positive integer | Error: `--timeout must be a positive integer`, exit 1 |
| `--poll-interval` not a positive integer | Error: `--poll-interval must be a positive integer`, exit 1 |
| CP submission fails | Write `<output>.failed`, exit 1 |
| Task times out (wait exceeds `--timeout`) | Write `<output>.failed` with `state=timed_out`, exit 1 |
| Output directory does not exist | Error: `Output directory does not exist: <dir>`, exit 1 |
## Security
- **No credentials in the script.** CP authentication (if required) is handled by `dispatch-task` reading `CP_AUTH_TOKEN` from the environment. The `bg-model-call` script only passes `--cp-url` and never handles tokens directly.
- **Prompt content is passed via file** (`--prompt-file`), never as a command-line argument. This avoids the prompt appearing in `ps` output.
## Examples
```bash
# Submit to MiniMax and poll in the background; check for result from a wakeup
echo "Review this spec for security issues: $(cat spec/auth.md)" > /tmp/minimax-prompt.txt
bg-model-call \
--model minimax \
--prompt /tmp/minimax-prompt.txt \
--output /tmp/minimax-result.txt \
--project my-project &
# Later (from ScheduleWakeup), check for completion:
if [[ -f /tmp/minimax-result.txt.done ]]; then
cat /tmp/minimax-result.txt
fi
# Dryrun to inspect resolved values
bg-model-call \
--model sonnet \
--prompt /tmp/my-prompt.txt \
--output /tmp/result.txt \
--dryrun
# Use a specific template instead of the model mapping
bg-model-call \
--template minimax-planning \
--prompt /tmp/prompt.txt \
--output /tmp/result.txt \
--dryrun
```