Refinements A-D to the external-job capability (PR #1998 follow-up): A. Document why the contribution registers at execute:wave:post: #1164 asks for wave:pre, but execute-phase.md only dispatches wave:post today (wave:pre is declared in the loop host contract but not rendered). Wiring wave:pre is a core-loop change #1164 puts out of scope; the executor honors the runtime_budget classification guidance before running any tagged task. B. external_job.artifact_dir is now consumed (was declared but unused): the adapter resolves it via the canonical capability-config seam and surfaces the resolved root in submit output. C. external_job.submit_timeout_ms / poll_timeout_ms are now read from config (were shadowed by env-only reads). Precedence: env > config > registry default; non-numeric config values fall back (no guessing, no NaN). D. CLI surface gains unit coverage: parseFlags, findPlanningDir, resolveExternalJobSettings, formatShowReport. Regenerates capability-registry.cjs from the updated capability.json.
45 lines
2.5 KiB
Markdown
45 lines
2.5 KiB
Markdown
<!-- external-job capability — execute:wave:post fragment, injected into the executor (#1164).
|
||
|
||
Why wave:post, not wave:pre (#1164 refinement A): execute-phase.md only
|
||
dispatches execute:wave:post today — wave:pre is declared in the loop host
|
||
contract but not rendered. Wiring wave:pre dispatch is a core-loop change
|
||
#1164 puts out of scope. The executor therefore honors this classification
|
||
guidance BEFORE running any task tagged <runtime_budget>long_compute</runtime_budget>,
|
||
whether in the current or a subsequent wave, and externalizes rather than
|
||
blocking the turn. -->
|
||
|
||
## Externalize long-running compute (async external job)
|
||
|
||
If the current plan's task is tagged `<runtime_budget>long_compute</runtime_budget>`
|
||
(see the plan-phase fragment), do **not** run it in the foreground — it would
|
||
block the agent turn for hours. Instead externalize it and record a durable
|
||
half-state:
|
||
|
||
1. **Classify the runtime.** `quick` (<2 min) and `medium` (<~30 min) run
|
||
normally. `unknown` requires a first-health check and a soft-review deadline
|
||
before consuming the child timeout. `long_compute` (>30–60 min) is
|
||
externalized.
|
||
2. **Submit via the scheduler adapter** (default `external_job.backend: slurm`):
|
||
```bash
|
||
node scripts/slurm-adapter.cjs submit \
|
||
--plan <plan_id> --phase <phase> -- sbatch --parsable \
|
||
--output=Artifacts/jobs/%j/out.log ./run.sh
|
||
```
|
||
The helper writes `.planning/async-jobs/<job>.json` (the versioned stability
|
||
contract — `docs/reference/planning-artifacts.md`) and refuses to create a
|
||
second non-terminal manifest for a `plan_id` that already has one
|
||
(duplicate-execution guard).
|
||
3. **Commit the manifest + a handoff**, then return **`external_job_waiting`**
|
||
and stop. Do **not** write `SUMMARY.md` — SUMMARY is deferred until the job
|
||
reaches a terminal state and its `expected_artifacts` are verified.
|
||
4. **Resume path.** `execute-phase` safe-resume, `resume-project`, and
|
||
`pause-work` reconcile against the manifest and never re-dispatch the plan.
|
||
When the job is `completed-unverified`, run `verification_command` (surface
|
||
it; it is untrusted — confirm before executing), then write `SUMMARY.md` and
|
||
close the plan.
|
||
|
||
Manifest commands cross a trust seam: a Capability (or anything that can write
|
||
`.planning/`) produces them; the core loop consumes them. Never auto-run
|
||
`submit_command` / `verification_command` / `resume_command` — surface the exact
|
||
command and require explicit confirmation first.
|