mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-09-19 08:11:52 +08:00
feat(build): add the route lever and route reporting (#2860)
Add a `workflow.route` customization (oneshot, full, auto; default auto) to bmad-build and bmad-build-auto. A route named in the invocation becomes `--set workflow.route=<value>` on the render command. Templates render only the pinned route's continuation; auto renders both plus the selection rule, byte-identical to before apart from reporting. Story frontmatter gains `route_source` (pinned or auto), and a pinned run halts when a resumed spec's recorded route differs. A pinned oneshot run does what it was told, how it was told: the mid-build stop conditions narrow to an intent gap. Work turning out larger than expected is the routing call the pin already made, and no longer stops the run. A hole in the intent still does, because the only alternative is to guess: bmad-build asks the human, bmad-build-auto halts blocked with `intent gap`. Give templates a `halt(message)` function so they can reject values they cannot act on; each build skill's workflow.md uses it to halt the render on a route outside oneshot, full, auto instead of silently rendering the auto path.
This commit is contained in:
@@ -9,6 +9,7 @@ Run the following command exactly once without changing the current working dire
|
||||
uv run --no-cache "{project-root}/_bmad/scripts/render_skill.py" --project-root "{project-root}" --skill "{skill-root}"
|
||||
```
|
||||
|
||||
- When the invocation names a route (`oneshot` or `full`), append `--set workflow.route=<value>` to the command.
|
||||
- On success, read and follow the one absolute `workflow.md` instruction printed to stdout.
|
||||
- If `{project-root}/_bmad/scripts/render_skill.py` is not found, this BMad installation is not set up yet: read the installed `bmad` skill's SKILL.md (a sibling of this skill's directory) and follow its setup flow, then run the command above once more.
|
||||
- On any other failure (including `uv` being unavailable), report the command output and HALT. Do not run any workflow source directly.
|
||||
|
||||
@@ -26,6 +26,12 @@ activation_steps_append = []
|
||||
|
||||
persistent_facts = []
|
||||
|
||||
# Execution route: "oneshot" implements in this session, "full" writes a spec
|
||||
# and hands implementation to a subagent, "auto" applies route_selection after
|
||||
# planning.
|
||||
|
||||
route = "auto"
|
||||
|
||||
# Automatic route selection. Replace the whole instruction to change the rule.
|
||||
|
||||
route_selection = """
|
||||
|
||||
@@ -3,7 +3,8 @@ title: '{title}'
|
||||
type: 'feature' # feature | bugfix | refactor | chore
|
||||
created: '{date}'
|
||||
status: 'draft' # draft | ready-for-dev | in-progress | in-review | done | blocked
|
||||
route: '' # oneshot | full — set by step-02's route-selection instruction
|
||||
route: '' # oneshot | full — set by step-02
|
||||
route_source: '' # pinned | auto — set with route by step-02
|
||||
review_loop_iteration: 0 # incremented by step-04 before each review loopback
|
||||
followup_review_recommended: false # set by step-04 on status: done — true if the LLM decided another review pass is worthwhile
|
||||
context: [] # optional: `{project-root}/`-prefixed paths to project-wide standards/docs the implementation agent should load. Keep short — only what isn't already distilled into the spec body.
|
||||
|
||||
@@ -8,13 +8,18 @@
|
||||
|
||||
1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<intent-contract>...</intent-contract>` block as `preserved_intent_contract`. Otherwise `preserved_intent_contract` is empty.
|
||||
2. Investigate codebase. _Read the code yourself for narrow, localized tasks. Isolate deep exploration in synchronous subagents: instruct them to give you distilled summaries only, and plan from those summaries._ Decide which findings actually matter for execution — the specific files, symbols/lines, reuse points, and read-only constraints — and carry those forward for the Code Map. This is where the investigation lands: the spec preserves it so it is never re-narrated to the implementer at dispatch time.
|
||||
{% if workflow.route == "oneshot" or workflow.route == "full" %}
|
||||
3. The route is `{{ workflow.route }}`; `route_source` is `pinned`.
|
||||
{% else %}
|
||||
3. {{ workflow.route_selection }}
|
||||
|
||||
Irreversible steps (migrations, data mutation, external side effects) always take the full route.
|
||||
|
||||
`route_source` is `auto`.
|
||||
{% endif %}
|
||||
4. Read `{{ rendered("spec-template.md") }}` fully, preserving all frontmatter fields and resolving `date` to the current system date.
|
||||
- **Oneshot:** set `route: 'oneshot'`.
|
||||
- **Full:** set `route: 'full'`. Drain the investigation into `## Code Map` — annotated paths, symbol/line anchors, reuse pointers, and read-only evidence — so the handoff need only point at the spec.
|
||||
- **Full:** set `route: 'full'`. Put what you learned into `## Code Map`: paths, symbols or lines, what to reuse, and what not to change. The subagent should be able to work from the spec without being told any of it again.
|
||||
|
||||
Set `route_source` from step 3.
|
||||
|
||||
If `{preserved_intent_contract}` is non-empty, substitute it for the `<intent-contract>` block before writing `{spec_file}`. Self-check against the route's READY FOR DEVELOPMENT standard.
|
||||
5. If intent gaps exist, do not fantasize and do not leave open questions. Multiple defensible readings of the intent that lead to observably different outcomes, with nothing in the intent to select between them, are an intent gap — do not resolve one by picking a reading. HALT with status `blocked`, blocking condition `intent gap`, and include the unanswered questions and evidence gathered.
|
||||
|
||||
@@ -22,12 +22,15 @@ Capture `baseline_revision` (current HEAD, or `NO_VCS` if version control is una
|
||||
|
||||
Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation. Execute only the matching route below, then continue with Both routes.
|
||||
|
||||
{% if workflow.route != "full" %}
|
||||
#### Oneshot (`route: oneshot`)
|
||||
|
||||
Implement in this main session from the story's Intent and working notes. Do not launch an implementing subagent or execute the full-route handoff. Append decisions, files touched, and surprises to `## Implementation Notes`.
|
||||
|
||||
Stop and replan if the intent left out something the user would notice in the result, you need to do something you cannot undo, or the remaining work is substantially larger than anticipated. Record the trigger in `## Implementation Notes`, set `route: 'full'` and `status: 'draft'`, then read fully and follow `{{ rendered("step-02-plan.md") }}`.
|
||||
Stop if the intent left out something the user would notice in the result. Record the gap in `## Implementation Notes`, then HALT with status `blocked` and blocking condition `intent gap` — do not guess.
|
||||
|
||||
{% endif %}
|
||||
{% if workflow.route != "oneshot" %}
|
||||
#### Full (`route: full`, or a legacy spec with no route)
|
||||
|
||||
Substitute the runtime placeholders (e.g. `{spec_file}`) into the implementation handoff below, then follow it verbatim. Do not add parent-authored goal restatements, file lists, ownership boundaries, or acceptance criteria to the handoff — the spec is the subagent's sole source of truth. If the handoff conflicts with the spec, HALT with status `blocked` and blocking condition `handoff conflicts with spec`, and include both conflicting passages.
|
||||
@@ -36,14 +39,17 @@ Substitute the runtime placeholders (e.g. `{spec_file}`) into the implementation
|
||||
|
||||
Invoke the subagent **synchronously** and wait for it to return in this same turn — do not background/detach it (`run_in_background`) or end your turn to await a notification (see workflow.md → Subagents). Resume at "Verify" only after it returns. If the platform allows, keep the subagent available for re-engagement after it returns — step-04 may send it review fixes.
|
||||
|
||||
{% endif %}
|
||||
### Both routes
|
||||
|
||||
**Path formatting rule:** Any markdown links written into `{spec_file}` must use paths relative to `{spec_file}`'s directory so they are clickable in VS Code. Any file paths displayed in terminal/conversation output must use CWD-relative format with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability. No leading `/` in either case.
|
||||
|
||||
### Verify
|
||||
|
||||
{% if workflow.route != "oneshot" %}
|
||||
On the full route, finish any unfinished work reported by the implementing subagent before proceeding.
|
||||
|
||||
{% endif %}
|
||||
Stage the diff and read it: using the repository's version-control tooling, write a unified diff of all changes since `{baseline_revision}` (from `{spec_file}` frontmatter) — untracked files included — to a uniquely-named file in the system temp directory, set `{diff_file}` to its absolute path, and read that file into your own context. Judge against the diff, not just implementation notes or a subagent's report.
|
||||
|
||||
Run the commands in `{spec_file}`'s `## Verification` section (or perform its manual checks). If verification fails and the failure cannot be fixed, HALT with status `blocked`, blocking condition `implementation verification failed`, and include the failing command or check and reason. When fixing a failure changes code, rewrite `{diff_file}` and re-read it. Acceptance criteria are judged at review, not here.
|
||||
|
||||
@@ -69,7 +69,15 @@ Announce skipped layers first, then launch every active layer before handling an
|
||||
5. Process entries in cascading order. If intent_gap exists, lower entries are moot; follow the intent_gap branch below. If bad_spec exists, lower entries are moot since code will be re-derived. If neither exists, process patch and defer normally. Before each bad_spec loopback, read `{spec_file}` frontmatter `review_loop_iteration` (missing means `0`), increment it by 1, and write it back. If it exceeds 5, append the triage-log entry for this pass, then HALT with status `blocked` and blocking condition `review repair loop exceeded 5 iterations (non-convergence)`.
|
||||
- **intent_gap** — Root cause is inside `<intent-contract>`. Save the attempted change as a patch file in `{{ config.implementation_artifacts }}` and reference it from the triage-log entry, then revert code changes. Append the triage-log entry for this pass, then HALT with status `blocked`, blocking condition `intent gap`, and include the unresolved questions and the saved patch path.
|
||||
- **bad_spec** — Root cause is outside `<intent-contract>`. Do not modify content inside `<intent-contract>`. Before reverting code: extract KEEP instructions for positive preservation (what worked well and must survive re-derivation). Revert code changes. Read the `## Spec Change Log` in `{spec_file}` and strictly respect all logged constraints when amending the sections outside `<intent-contract>` that contain the root cause. Append a new change-log entry recording: the triggering finding, what was amended, the known-bad state avoided, and the KEEP instructions. Append the triage-log entry for this pass, recording in each bad_spec row the amendment it triggered. Read fully and follow `{{ rendered("step-03-implement.md") }}` to re-derive the code, then this step will run again.
|
||||
- **patch** — Auto-fix. These are the only findings that survive loopbacks. On the full route, re-engage the step-03 implementation subagent — the same one, addressed by the name or id its launch returned; a fresh launch is not re-engagement. Send it one message, exactly this, with the findings filled in:
|
||||
- **patch** — Auto-fix. These are the only findings that survive loopbacks.
|
||||
{% if workflow.route == "oneshot" %}
|
||||
Apply the patches yourself.
|
||||
{% else %}
|
||||
{% if workflow.route == "full" %}
|
||||
Re-engage the step-03 implementation subagent — the same one, addressed by the name or id its launch returned; a fresh launch is not re-engagement. Send it one message, exactly this, with the findings filled in:
|
||||
{% else %}
|
||||
On the full route, re-engage the step-03 implementation subagent — the same one, addressed by the name or id its launch returned; a fresh launch is not re-engagement. Send it one message, exactly this, with the findings filled in:
|
||||
{% endif %}
|
||||
|
||||
```text
|
||||
Review of your implementation found problems. Fix each one below with the smallest change that does the job.
|
||||
@@ -79,7 +87,13 @@ Announce skipped layers first, then launch every active layer before handling an
|
||||
- <file> — <what is wrong> — <what the smallest fix must do>
|
||||
```
|
||||
|
||||
On oneshot, or if the full-route subagent cannot be continued, apply the patches yourself. Then re-run the commands in `{spec_file}`'s `## Verification` section (or perform its manual checks); if verification fails and the failure cannot be fixed, HALT with status `blocked` and blocking condition `patch verification failed`. Rewrite `{diff_file}` so it reflects the patched tree. Append the triage-log entry for this pass, recording in each patched row the fix applied.
|
||||
{% if workflow.route == "full" %}
|
||||
If the subagent cannot be continued, apply the patches yourself.
|
||||
{% else %}
|
||||
On oneshot, or if the full-route subagent cannot be continued, apply the patches yourself.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
Then re-run the commands in `{spec_file}`'s `## Verification` section (or perform its manual checks); if verification fails and the failure cannot be fixed, HALT with status `blocked` and blocking condition `patch verification failed`. Rewrite `{diff_file}` so it reflects the patched tree. Append the triage-log entry for this pass, recording in each patched row the fix applied.
|
||||
- **defer** — Update the single `deferred` list in `{spec_file}` frontmatter. If the field is absent (including on specs created before this field existed), add it once as an empty list. If it is `deferred: []`, replace that empty value when adding the first item; otherwise append to the existing list. Preserve every existing item, do not look for duplicates, and never add a second `deferred:` key. Serialize free-form values as YAML block scalars so characters such as `:`, `#`, quotes, and line breaks remain data. Each item uses this shape:
|
||||
```yaml
|
||||
deferred:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if workflow.route not in ("oneshot", "full", "auto") %}{{ halt("workflow.route must be oneshot, full, or auto, not " ~ workflow.route) }}{% endif %}
|
||||
# Build Auto Workflow
|
||||
|
||||
**Goal:** Turn intent into a hardened, reviewable artifact, without human interaction.
|
||||
|
||||
@@ -9,6 +9,7 @@ Run the following command exactly once without changing the current working dire
|
||||
uv run --no-cache "{project-root}/_bmad/scripts/render_skill.py" --project-root "{project-root}" --skill "{skill-root}"
|
||||
```
|
||||
|
||||
- When the invocation names a route (`oneshot` or `full`), append `--set workflow.route=<value>` to the command.
|
||||
- On success, read and follow the one absolute `workflow.md` instruction printed to stdout.
|
||||
- If `{project-root}/_bmad/scripts/render_skill.py` is not found, this BMad installation is not set up yet: read the installed `bmad` skill's SKILL.md (a sibling of this skill's directory) and follow its setup flow, then run the command above once more.
|
||||
- On any other failure (including `uv` being unavailable), report the command output and HALT. Do not run any workflow source directly.
|
||||
|
||||
@@ -26,6 +26,12 @@ activation_steps_append = []
|
||||
|
||||
persistent_facts = []
|
||||
|
||||
# Execution route: "oneshot" implements in this session, "full" writes a spec
|
||||
# and hands implementation to a subagent, "auto" applies route_selection after
|
||||
# planning.
|
||||
|
||||
route = "auto"
|
||||
|
||||
# Automatic route selection. Replace the whole instruction to change the rule.
|
||||
|
||||
route_selection = """
|
||||
|
||||
@@ -3,7 +3,8 @@ title: '{title}'
|
||||
type: 'feature' # feature | bugfix | refactor | chore
|
||||
created: '{date}'
|
||||
status: 'draft' # draft | ready-for-dev | in-progress | in-review | done
|
||||
route: '' # oneshot | full — set by step-02's route-selection instruction
|
||||
route: '' # oneshot | full — set by step-02
|
||||
route_source: '' # pinned | auto — set with route by step-02
|
||||
review_loop_iteration: 0 # incremented by step-04 before each review loopback
|
||||
context: [] # optional: `{project-root}/`-prefixed paths to project-wide standards/docs the implementation agent should load. Keep short — only what isn't already distilled into the spec body.
|
||||
---
|
||||
|
||||
@@ -19,7 +19,7 @@ Before listing artifacts, resolve existing workflow state in this order. Skip th
|
||||
Did the user pass a specific file path, spec name, or clear instruction this message?
|
||||
- If the user explicitly supplied a spec folder and a story id, with no specific spec file path, set `spec_folder` and `story_id`. Read `{spec_folder}/stories.yaml`; if it is missing or fails to parse, HALT rather than falling back to `{{ config.implementation_artifacts }}`. Find the one entry whose string `id` exactly equals `story_id`; if none exists, HALT rather than falling back. Use that entry's `title` and `description` as the starting intent.
|
||||
- Look for files matching `{spec_folder}/stories/{story_id}-*.md`. More than one match → HALT rather than choosing one. Exactly one match → set `spec_file` to that path and process it exactly as if the user had supplied that specific file path, including **Story-key resolution** and the existing status route below. No matches → derive a valid kebab-case slug from the entry's `title` (and `description` if needed), then set `spec_file` = `{spec_folder}/stories/{story_id}-{slug}.md` and proceed to INSTRUCTIONS.
|
||||
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file`. Before exiting, run **Story-key resolution** (below). Then **EARLY EXIT** to the appropriate step: `draft` → `{{ rendered("step-02-plan.md") }}`, `ready-for-dev`/`in-progress` → `{{ rendered("step-03-implement.md") }}` (or `{{ rendered("step-oneshot.md") }}` when `route` is `oneshot`), `in-review` → `{{ rendered("step-04-review.md") }}`. For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
||||
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file`. Before exiting, run **Story-key resolution** (below). Then **EARLY EXIT** to the appropriate step: `draft` → `{{ rendered("step-02-plan.md") }}`, {% if workflow.route == "oneshot" %}`ready-for-dev`/`in-progress` → `{{ rendered("step-oneshot.md") }}`{% elif workflow.route == "full" %}`ready-for-dev`/`in-progress` → `{{ rendered("step-03-implement.md") }}`, `in-review` → `{{ rendered("step-04-review.md") }}`{% else %}`ready-for-dev`/`in-progress` → `{{ rendered("step-03-implement.md") }}` (or `{{ rendered("step-oneshot.md") }}` when `route` is `oneshot`), `in-review` → `{{ rendered("step-04-review.md") }}`{% endif %}. For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
||||
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
||||
|
||||
2. Recent conversation
|
||||
@@ -31,8 +31,10 @@ Before listing artifacts, resolve existing workflow state in this order. Skip th
|
||||
- Resume one of the listed specs
|
||||
- **New** — start new work
|
||||
If `draft` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `{{ rendered("step-02-plan.md") }}` (resume planning from the draft)
|
||||
If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `{{ rendered("step-03-implement.md") }}` (or `{{ rendered("step-oneshot.md") }}` when `route` is `oneshot`)
|
||||
If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → {% if workflow.route == "oneshot" %}`{{ rendered("step-oneshot.md") }}`{% elif workflow.route == "full" %}`{{ rendered("step-03-implement.md") }}`{% else %}`{{ rendered("step-03-implement.md") }}` (or `{{ rendered("step-oneshot.md") }}` when `route` is `oneshot`){% endif +%}
|
||||
{% if workflow.route != "oneshot" %}
|
||||
If `in-review` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `{{ rendered("step-04-review.md") }}`
|
||||
{% endif %}
|
||||
If the user chooses **New**: proceed to INSTRUCTIONS
|
||||
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
|
||||
|
||||
|
||||
@@ -11,16 +11,24 @@
|
||||
2. Investigate the codebase. When you can, send deep searches to subagents and wait for them in this turn. Tell them to return short summaries only, so this session does not fill up with their notes. Keep only what the work needs: the specific files, symbols or lines, what to reuse, and what not to change. Write that into the Code Map. Do not retell the investigation when implementation starts — the spec already has it.
|
||||
|
||||
Do not ask the human during investigation. When something is unclear, look in the repository, planning artifacts, or history first. Keep looking until you know, or until those sources have nothing more to say. Leave any remaining choice for the next step.
|
||||
{% if workflow.route == "oneshot" %}
|
||||
3. Read `{{ rendered("spec-template.md") }}` fully and write `{spec_file}`.
|
||||
Set `route: 'oneshot'`, `route_source: 'pinned'`, and `status: 'in-progress'`, resolving `date` to the current system date.
|
||||
If `preserved_intent` is non-empty, use it as the frozen block.
|
||||
**EARLY EXIT** → `{{ rendered("step-oneshot.md") }}`.
|
||||
{% elif workflow.route == "full" %}
|
||||
3. Set `route: 'full'` and `route_source: 'pinned'`, then continue.
|
||||
{% else %}
|
||||
3. {{ workflow.route_selection }}
|
||||
|
||||
Intent gaps and irreversible steps (migrations, data mutation, external side effects) always take the full path below.
|
||||
|
||||
For oneshot with intent resolved: read `{{ rendered("spec-template.md") }}` fully and write `{spec_file}`.
|
||||
Set `route: 'oneshot'` and `status: 'in-progress'`, resolving `date` to the current system date.
|
||||
For oneshot: read `{{ rendered("spec-template.md") }}` fully and write `{spec_file}`.
|
||||
Set `route: 'oneshot'`, `route_source: 'auto'`, and `status: 'in-progress'`, resolving `date` to the current system date.
|
||||
If `preserved_intent` is non-empty, use it as the frozen block.
|
||||
**EARLY EXIT** → `{{ rendered("step-oneshot.md") }}`.
|
||||
|
||||
For full, set `route: 'full'` and continue.
|
||||
For full, set `route: 'full'` and `route_source: 'auto'`, then continue.
|
||||
{% endif %}
|
||||
{% if workflow.route != "oneshot" %}
|
||||
4. Read `{{ rendered("spec-template.md") }}` fully. Fill it out from the intent and investigation, resolving the template's `date` field to the current system date. Put the investigation into `## Code Map`: paths, symbols or lines, what to reuse, and what not to change. Implementation should work from the spec without being told the investigation again. If there are intent gaps, add a `## Open Questions` section with one entry per gap: the choice, the options, and what each option means. Never write an intent gap into the frozen block as an assumption. If `preserved_intent` is non-empty, replace the `<frozen-after-approval>` block with it before writing. Write the result to `{spec_file}`.
|
||||
5. Self-review against READY FOR DEVELOPMENT standard. For anything important that's missing: if the repository can tell you, go look and fix the spec; if a human has to decide, add an `## Open Questions` entry. Do not invent the answer.
|
||||
6. Resolve the gates before the checkpoint. Two things must be settled, in whatever order the conversation makes natural; combine them in one message when both apply.
|
||||
@@ -61,3 +69,4 @@ Before acting on approval, re-read `{spec_file}` from disk. If it is missing, HA
|
||||
## NEXT
|
||||
|
||||
Read fully and follow `{{ rendered("step-03-implement.md") }}`
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if workflow.route != "oneshot" %}
|
||||
---
|
||||
---
|
||||
|
||||
@@ -48,3 +49,4 @@ If `{spec_file}`'s `<frozen-after-approval>` block contains an I/O & Edge-Case M
|
||||
## NEXT
|
||||
|
||||
Read fully and follow `{{ rendered("step-04-review.md") }}`
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if workflow.route != "oneshot" %}
|
||||
# Step 4: Review
|
||||
|
||||
## RULES
|
||||
@@ -82,3 +83,4 @@ If a layer's instruction requires subagents and none are available, for each suc
|
||||
## NEXT
|
||||
|
||||
Read fully and follow `{{ rendered("step-05-present.md") }}`
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if workflow.route != "oneshot" %}
|
||||
---
|
||||
---
|
||||
|
||||
@@ -40,3 +41,4 @@ Workflow complete.
|
||||
If anything appears below, follow it as the final terminal instruction before exiting; otherwise exit normally.
|
||||
|
||||
{{ workflow.on_complete }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if workflow.route != "full" %}
|
||||
# Step One-Shot: Implement, Review, Present
|
||||
|
||||
You reach this step from step 2, or from step 1 when resuming a spec whose `route` is `oneshot`. `{spec_file}` already exists.
|
||||
@@ -15,15 +16,15 @@ You reach this step from step 2, or from step 1 when resuming a spec whose `rout
|
||||
|
||||
If `{story_key}` is not empty and `{{ config.implementation_artifacts }}/sprint-status.yaml` exists, read `{{ rendered("sync-sprint-status.md") }}` with `{target_status}` = `in-progress`.
|
||||
|
||||
If intent gaps remain, present each as a numbered question with its options and what each option means, HALT for the human's answers, and fold the answers into the Intent.
|
||||
|
||||
Build the change from `{spec_file}`. The Intent section is what you implement. As you work, add notes to `## Implementation Notes`: decisions you made, files you changed, surprises.
|
||||
|
||||
**When to stop and replan.** Stop coding if you learn something step 2 did not account for:
|
||||
|
||||
- the request left out something the user would notice in the result
|
||||
- you need to do something you cannot undo
|
||||
- the remaining work is substantially larger than anticipated
|
||||
|
||||
Write what triggered the stop in `## Implementation Notes`. Then update `{spec_file}`: add back `## Code Map` (filled in from what you learned while implementing) and `## Open Questions` (one question per gap), set `route: 'full'` and `status: 'draft'`. Go back to `{{ rendered("step-02-plan.md") }}` step 6.
|
||||
{% if workflow.route == "oneshot" %}
|
||||
**When to stop.** Stop coding if the request left out something the user would notice in the result. Write the gap in `## Implementation Notes`, then ask the human — do not guess.
|
||||
{% else %}
|
||||
**When to stop and replan.** Stop coding if the request left out something the user would notice in the result. Write the gap in `## Implementation Notes`. Then update `{spec_file}`: add back `## Code Map` (filled in from what you learned while implementing) and `## Open Questions` (one question per gap), set `route: 'full'` and `status: 'draft'`. Go back to `{{ rendered("step-02-plan.md") }}` step 6.
|
||||
{% endif %}
|
||||
|
||||
### Review
|
||||
|
||||
@@ -104,3 +105,4 @@ Workflow complete.
|
||||
If anything appears below, do it before exiting. Otherwise exit.
|
||||
|
||||
{{ workflow.on_complete }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if workflow.route not in ("oneshot", "full", "auto") %}{{ halt("workflow.route must be oneshot, full, or auto, not " ~ workflow.route) }}{% endif %}
|
||||
# Build New Preview Workflow
|
||||
|
||||
**Goal:** Turn user intent into a hardened, reviewable artifact.
|
||||
|
||||
@@ -400,8 +400,14 @@ class _RenderContext:
|
||||
self,
|
||||
),
|
||||
"rendered": self._rendered,
|
||||
"halt": self._halt,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _halt(message: Any) -> str:
|
||||
"""Let a template reject its inputs; the caller prefixes the source and line."""
|
||||
raise RenderError(str(message))
|
||||
|
||||
@jinja2.pass_context
|
||||
def _rendered(self, context: jinja2.runtime.Context, target: Any) -> str:
|
||||
if not isinstance(target, str) or target not in self._source_names:
|
||||
|
||||
@@ -451,6 +451,7 @@ class RenderSkillTests(unittest.TestCase):
|
||||
"{% for item in workflow.count %}{{ item }}{% endfor %}\n",
|
||||
'{{ rendered("missing.md") }}\n',
|
||||
"{{ rendered(workflow.items) }}\n",
|
||||
'{{ halt("rejected") }}\n',
|
||||
)
|
||||
for template in templates:
|
||||
with self.subTest(template=template):
|
||||
@@ -470,6 +471,13 @@ class RenderSkillTests(unittest.TestCase):
|
||||
rs.RenderError, r"^detail\.md:1: missing customization parameter `workflow\.missing`$"
|
||||
):
|
||||
rs.render(ws.project, skill)
|
||||
guard = '{% if workflow.value not in ("two", "three") %}{{ halt("value must be two or three, not " ~ workflow.value) }}{% endif %}\n'
|
||||
(skill / "detail.md").write_text(guard + "kept\n", encoding="utf-8")
|
||||
with self.assertRaisesRegex(rs.RenderError, r"^detail\.md:1: value must be two or three, not one$"):
|
||||
rs.render(ws.project, skill)
|
||||
self.assertEqual(
|
||||
_files(rs.render(ws.project, skill, assignments=["workflow.value=two"]).parent)["detail.md"], b"kept\n"
|
||||
)
|
||||
|
||||
def test_excluded_entry_and_surviving_reference_to_excluded_file_halt(self):
|
||||
for workflow in (
|
||||
|
||||
@@ -83,11 +83,12 @@ Instructions for the full route.
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Templates see three names:
|
||||
Templates see four names:
|
||||
|
||||
- `config` — the central config. `config.key` is the one scalar with that key anywhere in the merged config (an ambiguous or missing key halts); `config.a.b.c` names an explicit path. `{project-root}` in the value is bound.
|
||||
- `workflow` — the effective customization's `[workflow]` table: shipped `customize.toml`, then project and user TOML, then invocation overrides. Each value is validated against the shape of its shipped default. Inserted directly, a string list renders as a Markdown list and a list of lens tables as lens sections, the same output the pre-Jinja2 tokens produced; `{% for %}` iterates either. `{skill-root}` in a value is bound to the generation directory.
|
||||
- `rendered("file.md")` — the generation path of another rendered source. The target must be a Markdown file in the skill other than `SKILL.md`, which the renderer excludes.
|
||||
- `halt(message)` — stops the render with that message, prefixed by the source and line. Use it to reject a customization value the templates cannot act on, such as a misspelled selector.
|
||||
|
||||
Every value reached during the render is part of the generation's identity. Customization values are inserted as opaque text and never re-parsed as templates. An undefined name, a table inserted as a value, a loop over a non-list, or a syntax error halts the render with `file:line`. A secondary file whose rendered body is whitespace is left out of the snapshot, and a surviving `rendered()` link to it halts; `workflow.md` rendering to nothing halts. Agent-facing placeholders such as `{{epic_number}}` must sit inside `{% raw %}…{% endraw %}` in a rendered skill.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user