okstra 0.155.0 → 0.157.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/docs/architecture.md +3 -1
  2. package/docs/for-ai/skills/okstra-schedule-gen.md +5 -4
  3. package/docs/project-structure-overview.md +7 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/prompts/profiles/_common-contract.md +1 -1
  7. package/runtime/python/okstra_ctl/clarification_items.py +3 -3
  8. package/runtime/python/okstra_ctl/render_final_report.py +31 -44
  9. package/runtime/python/okstra_ctl/report_contract.py +15 -0
  10. package/runtime/python/okstra_ctl/report_finalize.py +22 -3
  11. package/runtime/python/okstra_ctl/report_markdown.py +441 -0
  12. package/runtime/python/okstra_ctl/schedule_semantics.py +186 -91
  13. package/runtime/python/okstra_ctl/stage_map.py +203 -1
  14. package/runtime/python/okstra_ctl/wizard.py +1 -11
  15. package/runtime/python/okstra_project/state.py +14 -2
  16. package/runtime/skills/okstra-schedule-gen/SKILL.md +43 -18
  17. package/runtime/templates/reports/final-report-v2.template.md +74 -10
  18. package/runtime/templates/reports/md/macros/sections.md +19 -0
  19. package/runtime/templates/reports/md/tasks/change-impact-analysis.template.md +18 -0
  20. package/runtime/templates/reports/md/tasks/error-analysis.template.md +13 -0
  21. package/runtime/templates/reports/md/tasks/feature-analysis.template.md +13 -0
  22. package/runtime/templates/reports/md/tasks/final-verification.template.md +13 -0
  23. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +15 -0
  24. package/runtime/templates/reports/md/tasks/implementation.template.md +15 -0
  25. package/runtime/templates/reports/md/tasks/improvement-discovery.template.md +10 -0
  26. package/runtime/templates/reports/md/tasks/project-analysis.template.md +15 -0
  27. package/runtime/templates/reports/md/tasks/release-handoff.template.md +13 -0
  28. package/runtime/templates/reports/md/tasks/requirements-discovery.template.md +15 -0
  29. package/runtime/templates/reports/schedule.template.md +166 -63
  30. package/runtime/validators/validate-run.py +19 -6
  31. package/runtime/validators/validate-schedule.py +94 -65
  32. package/src/commands/inspect/stage-map.mjs +6 -1
  33. package/src/commands/inspect/worker-liveness.mjs +15 -3
  34. package/src/commands/lifecycle/install.mjs +69 -4
  35. package/src/commands/lifecycle/uninstall.mjs +21 -35
  36. package/src/lib/install-assets.mjs +37 -0
@@ -77,7 +77,6 @@ from okstra_ctl.run import (
77
77
  from okstra_ctl.stage_map import (
78
78
  StageMapError,
79
79
  parse_stage_map_file,
80
- parse_stage_map_text,
81
80
  stage_map_records,
82
81
  )
83
82
  from okstra_ctl.user_response import (
@@ -1034,16 +1033,7 @@ def _fix_cycle_confirm_required(state: WizardState) -> bool:
1034
1033
  def _parse_stage_objects(state: WizardState) -> list:
1035
1034
  """Return the approved plan's strict Stage Map objects for the picker."""
1036
1035
  try:
1037
- plan_text = Path(state.approved_plan_path).read_text(encoding="utf-8")
1038
- return parse_stage_map_text(
1039
- plan_text,
1040
- source_plan_path=str(Path(state.approved_plan_path).resolve()),
1041
- )
1042
- except OSError as exc:
1043
- raise WizardError(
1044
- f"approved plan 의 Stage Map 을 읽을 수 없습니다 "
1045
- f"({state.approved_plan_path}): {exc}"
1046
- ) from exc
1036
+ return parse_stage_map_file(Path(state.approved_plan_path))
1047
1037
  except StageMapError as exc:
1048
1038
  raise WizardError(
1049
1039
  f"approved plan 의 Stage Map 을 신뢰할 수 없습니다 "
@@ -384,12 +384,23 @@ def stage_map_read_side_snapshot(project_root: Path, task_key: str) -> dict:
384
384
  """
385
385
  from okstra_ctl.consumers import read_stage_consumer_state
386
386
  from okstra_ctl.paths import RunRef
387
- from okstra_ctl.stage_map import StageMapError, load_task_stage_map
387
+ from okstra_ctl.stage_map import (
388
+ PlanningDetail,
389
+ StageMapError,
390
+ load_planning_detail,
391
+ load_task_stage_map,
392
+ merge_planning_detail,
393
+ )
388
394
 
389
395
  identity = resolve_task_identity(project_root, task_key)
390
396
  task_root = Path(identity["taskRoot"])
391
397
  try:
392
398
  stage_snapshot = load_task_stage_map(task_root, identity["manifest"])
399
+ detail = (
400
+ load_planning_detail(Path(stage_snapshot.source_plan_path))
401
+ if stage_snapshot.source_plan_path
402
+ else PlanningDetail({}, {})
403
+ )
393
404
  except StageMapError as exc:
394
405
  raise StateError(str(exc), stage=exc.code) from exc
395
406
  plan_run_root = RunRef.from_task_root(
@@ -407,8 +418,9 @@ def stage_map_read_side_snapshot(project_root: Path, task_key: str) -> dict:
407
418
  "taskRoot": identity["taskRoot"],
408
419
  "state": stage_snapshot.state,
409
420
  "sourcePlanPath": stage_snapshot.source_plan_path,
410
- "stages": stage_snapshot.stages,
421
+ "stages": merge_planning_detail(stage_snapshot.stages, detail),
411
422
  "doneStages": done,
423
+ "planning": detail.task_narratives,
412
424
  }
413
425
 
414
426
 
@@ -49,13 +49,13 @@ If the call fails with `unknown command: preflight`, the `okstra` binary on PATH
49
49
 
50
50
  ## Contract SSOT — template + validator
51
51
 
52
- The installed template `~/.okstra/templates/reports/schedule.template.md` is the **byte-for-byte SSOT** for the output shape: frontmatter, top header block, the mandatory `##` heading list and order, per-task `Item / Detail` field labels and sub-section order, table column shapes, the ASCII Gantt format (relative day axis, plain fence, `█`/`░`/`! crit`/`est` legend), the dependency-graph shapes, and the optional `## Glossary` gate. **Read the template before writing the schedule and follow it exactly** — do not re-derive section shapes from memory. Headings and field labels stay English literals regardless of the source-report language; body prose is Korean. When a section has no data, render its heading with `_none_` — never delete or reorder headings. Never emit mermaid or any graph DSL.
52
+ The installed template `~/.okstra/templates/reports/schedule.template.md` is the **byte-for-byte SSOT** for the output shape: frontmatter, top header block, the mandatory `##` heading list and order, per-task `Item / Detail` field labels and sub-section order, table column shapes, the ASCII Gantt format (relative day axis, plain fence, bar-only rows), the dependency-graph shapes, and the optional `## Glossary` gate. **Read the template before writing the schedule and follow it exactly** — do not re-derive section shapes from memory. Headings and field labels stay English literals regardless of the source-report language; body prose is Korean. When a section has no data, render its heading with `_none_` — never delete or reorder headings. Never emit mermaid or any graph DSL.
53
53
 
54
54
  `~/.okstra/lib/validators/validate-schedule.py` is the enforcement for all of the above (heading order, field labels, controlled vocabulary — e.g. `Med-High` is the canonical risk form — forbidden translations, checkbox bans, Gantt fence rules, unresolved-code detection). The Step 4 ambiguous-classification rationale line is the one rule the validator does not yet enforce — emit it yourself.
55
55
 
56
56
  One computation rule the template scaffold cannot carry inline:
57
57
 
58
- - **Effort-to-Day mapping**: day ranges per size are defined once in the template's `### Effort Sizing Criteria` table. For the At a Glance totals line, sum that table's lower bounds across in-scope tasks for the lower total and upper bounds for the upper total.
58
+ - **Effort-to-Day mapping**: day ranges per size are defined once in the template's `### Effort Sizing Criteria` table, and a task's `Days` cell takes its range from that row. `XXL` is the exception — it has no finite upper bound there, so an XXL task is sized by its own stage decomposition and its `Days` cell is the sum of its Work Breakdown rows. The At a Glance totals line sums every in-scope task's `Days`, XXL included. Append ` (est)` to a `Days` range the schedule derived rather than read from the plan; implementation-planning states no durations, so that is every range today.
59
59
 
60
60
  ## Procedure
61
61
 
@@ -85,7 +85,7 @@ For each in-scope task, the **authoritative source is its `implementation-planni
85
85
  okstra stage-map <task-key> --json
86
86
  ```
87
87
 
88
- The response is source-aware: `{ok, taskKey, taskRoot, state, sourcePlanPath, stages:[{stage_number,title,depends_on,step_count}], doneStages:[int]}`. Branch on it explicitly:
88
+ The response is source-aware: `{ok, taskKey, taskRoot, state, sourcePlanPath, stages:[…], doneStages:[int], planning:{…}}`. Each stage row carries `stage_number`, `title`, `depends_on`, `step_count`, `exit_contract_summary` and — from a schema-v2 report — that stage's `stepwiseExecution` (the numbered steps, each with `action`, `files`, `command`, `expected`), `sliceValue`, `acceptance` and `exitContract`. `planning` carries the report's task-level rows: `rollbackStrategy`, `validationChecklist`, `crossProjectDependencies`, `dependencyMigrationRisk`, `recommendedOption`. Branch on it explicitly:
89
89
 
90
90
  The resolved source, when ready, is a report under `runs/implementation-planning`; the CLI owns choosing exactly one report from that domain.
91
91
 
@@ -93,7 +93,7 @@ The resolved source, when ready, is a report under `runs/implementation-planning
93
93
  - `ok: true, state: "missing"` — record an empty source and empty stage sets. Tag the task `[NEEDS-PLANNING]`, skip the stage picker, and render task-level metadata only, with no forward Work Breakdown row, Gantt row, or day total.
94
94
  - `ok: false` or any state other than `ready` and `missing` — this is a structured Stage Map error, such as a corrupt or conflicting plan source. Stop before drafting, preserve the CLI `stage` and `reason` in the user-facing error, and do not guess from another report.
95
95
 
96
- For `ready`, read task-level Priority / Risk / Scope / Repos from `sourcePlanPath`. Read header and phase-bucket metadata from `task-manifest.json`: `taskId`, `taskGroup`, `taskKey`, `workCategory`, `workStatus`, `taskType`, and `workflow.currentPhase`. Blocking and approval items are not extracted (see Audience & authority).
96
+ For `ready`, the CLI response is the source for everything the schedule renders — stage rows and `planning` together. Read `sourcePlanPath` only for the task-level Priority / Risk / Scope / Repos that the payload does not carry, and read no further into it: transcribing the payload is the whole point of Step 4. Read header metadata from `task-manifest.json`: `taskId`, `taskGroup`, `taskKey`, `workCategory`, `workStatus`, `taskType`, and `workflow.currentPhase`. Blocking and approval items are not extracted (see Audience & authority).
97
97
 
98
98
  If unfinished stages are empty while `workStatus` is not `done`, render `_Complete — no remaining stage_` under the task's phase section and contribute no forward day total.
99
99
 
@@ -113,28 +113,44 @@ Run this **once per in-scope task that has a non-empty `remainingStages`**, sequ
113
113
 
114
114
  Record the chosen `selectedStages` for this task. Reject and re-prompt any custom selection whose transitive `depends_on` closure is not covered by `selectedStages ∪ doneStages`.
115
115
 
116
- ### Step 4: Phase classification
116
+ ### Step 4: Task ordering and per-task transcription
117
117
 
118
118
  The canonical categories come from `scripts/okstra_ctl/work_categories.py::WORK_CATEGORIES`. Do not infer or publish another category.
119
119
 
120
- | workCategory | Default phase |
121
- |--------------|---------------|
122
- | `bugfix` | Phase 1 when risk is High/Med-High; otherwise Phase 2 |
123
- | `feature` / `improvement` | Phase 2 |
124
- | `refactor` / `ops` | Phase 3 |
125
- | unmatched or missing | Phase 2, with rationale `> _workCategory '<raw-value>' undefined — defaulting to Phase 2._` at the top of that phase section |
120
+ **There are no phase buckets and no priority table.** `## Task Details` holds one `### <n>. <Title>` block per in-scope task, numbered from 1 in At a Glance order. The heading is the work's name — the task-id is a machine key and belongs in the At a Glance row, not in a heading the reader has to decode; the block binds to its row by number. At a Glance is already sorted Priority-first, so that ordering carries priority; sequence lives in the stages; and `## Recommended Immediate Actions` is the one place that says what to do next. In the per-task `Item / Detail` table, `Status` says where the work stands in plain words — "구현 계획 승인됨 — 실행 미착수", not `implementation-planning / implementation-planning`. Derive it from `taskType` and `workflow.currentPhase`, but never print those tokens: they are okstra's phase names and the reader has no phases.
126
121
 
127
- Priority overrides category: `P0` → Phase 1; `P1`/`P2` → Phase 2; `P3` or multi-repo + infrastructure scope → Phase 3. When still ambiguous, place the task in the closest phase and add a one-line rationale at the top of that phase section (not validator-enforced — emit it yourself).
122
+ Order At a Glance by Priority (`P0` first), breaking ties by `workCategory` in this canonical order: `bugfix`, `feature`, `refactor`, `ops`, `improvement`. A task whose `workCategory` is unmatched or missing sorts last within its priority and keeps its raw value in the `Category` cell — never substitute a category that is not in that list.
128
123
 
129
- Phase bucketing stays **task-level** (a task lands in one Phase by its `workCategory`/Priority). `workStatus` is only a Step 2 candidate filter. In the per-task `Item / Detail` table, render `Status` as `<taskType> / <currentPhase>`.
124
+ Within a task's block, selected stages become the Work Breakdown rows under the exact header `| Stage | Title | Steps | Depends On | Days |`. Render one row per selected stage using its Stage Map `title`, `step_count`, and `depends_on`, plus its proportional day range. Stages excluded because they are already done are listed once as `> _Done stages: stage <n>, …_` and carry no forward effort.
130
125
 
131
- Within a task's per-task section, selected stages become the Work Breakdown rows under the exact header `| Stage | Title | Steps | Depends On | Days |`. Render one row per selected stage using its Stage Map `title`, `step_count`, and `depends_on`, plus its proportional day range. Stages excluded because they are already done are listed once as `> _Done stages: stage <n>, …_` and carry no forward effort.
126
+ **Write the schedule FOR the team executing it, FROM the plan.** Two different duties:
127
+
128
+ - **Facts are carried exactly.** File paths, commands, counts, day ranges, rollback triggers, the number of specs or tables — a wrong path or a dropped item is a defect. This is what a hand-written summary of the report loses, and why `okstra stage-map` hands them over structurally.
129
+ - **Prose is rewritten for the reader doing the work.** Say what the step does, why it exists, and what they should see when it succeeds. Do NOT paste a source field wholesale: the planning report was written to justify a design to a reviewer, so it carries sentences arguing for the approach — `sliceValue` is exactly that — which tell an implementer nothing. A schedule full of them reads as a record of someone else's deliberation.
130
+
131
+ Selecting which source rows belong in a schedule is not summarising. Dropping a justification is correct; dropping a file path is not.
132
+
133
+ **The reader has no access to the plan's review history.** A sentence like "this answers the objection that sank the direction the first time" or "the reporter's rationale is …" assumes the reader watched the plan being argued. State the engineering reason directly — *why this order, what breaks without it* — and never cite a prior revision, a reviewer, or a decision's provenance. The same applies to okstra's own vocabulary: `task-group`, `implementation-planning`, a `taskType` printed as a Status, a sibling task-id nobody outside the run can look up. Every enum the schedule prints is either defined on the page or replaced by words — the `Kind` / `Direction` labels on the risk and dependency tables classify rows for the plan's author and tell an implementer nothing.
134
+
135
+ **Stage titles and every table cell are written in the schedule's prose language.** Headings and field labels stay English literals; the content beside them does not. A Work Breakdown whose `Title` column is English next to a `Steps` table in Korean makes the reader switch languages inside one block. The selection contract carries the same titles the schedule prints, so the two stay comparable.
136
+
137
+ **A stage block is `Steps` plus `Exit criteria`, and nothing else.** The `Steps` table comes from `stepwiseExecution` — one row per step with what it does (keep the leading `RED:` / `GREEN:` marker), the files it touches, and what the executor should see when it succeeds, from that step's `expected`. `Exit criteria` is one line from `exitContract` saying when the stage is finished.
138
+
139
+ Do not render `sliceValue` — it argues why the plan sliced the work this way, which is a reviewer's question, not an implementer's. Do not render `acceptance` alongside `exitContract`: they restate each other. Measured on one real plan, `acceptance`'s four claims all reappeared in `exitContract`, which then added two more. Sort `Verification Commands` `pre` → `mid` → `post` and introduce those three values above the table — source order interleaves them, and a reader working down the table would run a precondition after the checks it gates.
140
+
141
+ **Transcribing means the content, never the source row's `id`.** `VC-001`, `RB-003`, `XP-001`, `DM-002`, `EO-015` and their kin index a report the schedule's reader cannot open, so the self-contained rule rejects them and the validator enforces it. Drop the id column, and where a row's body cites a sibling code, replace the citation with the thing it names ("the boot probe asserts all 29 keys", not "VC-006").
132
142
 
133
143
  ### Step 5: Gantt decision (render by default)
134
144
 
135
- `## Gantt Chart` is **rendered by default** — skip ONLY when literally no day signal exists (every task is effort=XXL with no visible decomposition, or all tasks lack both effort sizing and decomposition). Render whenever any of these hold: 2+ tasks with effort sizing; 1 task whose effort yields a range (mid-point bar, or `lo`/`hi` two-bar form); 1 task with Part/Phase/Step decomposition in the source (bars at decomposition-unit level); total estimated effort ≥ 3 days. When per-unit day allocations aren't itemized, split the parent range across the visible units yourself and append the `est` annotation (or add `> Per-day allocation is an estimate; refresh recommended after blocking items are resolved.`). "Range is wide", "single task", "user decisions pending" are NOT skip reasons — render an estimate-tagged chart instead.
145
+ `## Gantt Chart` is **rendered by default** — skip ONLY when literally no day signal exists (every in-scope task lacks both effort sizing and stage decomposition). An XXL task is not a skip reason: it is sized by its stages. Render whenever any of these hold: 2+ tasks with effort sizing; 1 task whose effort yields a range (mid-point bar, or `lo`/`hi` two-bar form); 1 task with Part/Phase/Step decomposition in the source (bars at decomposition-unit level); total estimated effort ≥ 3 days. When per-unit day allocations aren't itemized, split the parent range across the visible units yourself and append the `est` annotation (or add `> Per-day allocation is an estimate; refresh recommended after blocking items are resolved.`). "Range is wide", "single task", "user decisions pending" are NOT skip reasons — render an estimate-tagged chart instead.
146
+
147
+ When the source is a Stage Map, the Gantt bars are selected stages. A row is labelled `Stage <n>` when exactly one task is scheduled, and `<TASK-ID> Stage <n>` when more than one is. Spell the stage out — `S1` costs the reader a lookup and saves five characters. A row is a label and a bar and nothing else: no `days=` annotation (the Work Breakdown owns those numbers, and the validator compares the bar against that column), and no per-row `! crit` / `est` markers, which distinguish nothing when every row carries them. One line above the fence states the column unit and what `█` and `░` mean — that is the whole legend, because those two glyphs are the only notation left. Split the task effort range proportionally by `step_count`; round every stage except the last to 0.5 day and let the last stage absorb the remainder. Cross-stage dependency annotations follow `depends_on`. Already-done and non-selected stages never get a bar. A task tagged `[NEEDS-PLANNING]` contributes no bars.
148
+
149
+ **Bar geometry is checked, not decorative.** One column is half a day: a bar runs `lower / 0.5` filled cells `█` then `(upper - lower) / 0.5` open cells `░`. A bar drawn to any other width is a validation error, as is an axis whose last tick overshoots the scheduled work by 5 days or more.
150
+
151
+ **Every controlled code the schedule prints must be defined on the page.** The template's `### Priority & Risk Scale` resolves `P0`–`P3` and `Very Low`–`High`; the validator requires that subsection and both header literals. Stage labels are spelled out for the same reason — Gantt rows read `Stage 3`, and `Depends On` cells read `None` / `Stage 2` / `Stage 1 (done)` rather than a bare number the reader has to match against the Stage column of the same table. A reader who never saw the source report cannot rank `P0` against `P1`, calibrate `High`, or expand `S3`; those are opaque codes wearing a friendlier shape.
136
152
 
137
- When the source is a Stage Map, the Gantt bars are selected stages. Every row identifier is `<TASK-ID>/S<stage-number>` and carries `days=<lower>~<upper>` matching the Work Breakdown row. Split the task effort range proportionally by `step_count`; round every stage except the last to 0.5 day and let the last stage absorb the remainder. Cross-stage dependency annotations follow `depends_on`. Already-done and non-selected stages never get a bar. A task tagged `[NEEDS-PLANNING]` contributes no bars.
153
+ **`## Task Dependency Graph` is optional and carries cross-task edges only.** Stage order already lives in the Work Breakdown's `Depends On` column; drawing it again as an arrow list makes the reader cross-reference one table against another to learn nothing new. Omit the section entirely — heading included — when no task depends on another, which is always true of a single-task schedule. A heading whose body only redirects to another table is noise, the same defect as a priority table that restates At a Glance.
138
154
 
139
155
  An unrepresentable half-day allocation is a validation error. Do not switch to a fallback allocation algorithm or publish that draft.
140
156
 
@@ -182,7 +198,16 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart skip
182
198
  ```
183
199
 
184
200
  3. **Run the deterministic gate first.** Execute `python3 ~/.okstra/lib/validators/validate-schedule.py <draft> --selection-json <selection>`. Do not dispatch the narrative verifier when this exits non-zero.
185
- 4. **Run the independent LLM verifier second.** Only after the deterministic gate passes, dispatch an independent verifier subagent with the draft and selection JSON, but without the lead's reasoning. It checks client-facing coherence, phase rationale, executable ordering, engineering-only scope, and whether the prose contradicts the structured schedule. It returns `pass` plus concrete findings.
201
+ 4. **Run the independent LLM verifier second.** Only after the deterministic gate passes, dispatch an independent verifier subagent with the draft and selection JSON, but without the lead's reasoning. It returns `pass` plus concrete findings. Give it these checks:
202
+ - **Executable ordering** — stage sequence, `Depends On`, and Gantt bar positions tell the same story; nothing depends on something scheduled after it.
203
+ - **Arithmetic** — the Work Breakdown `Days` column sums to the At a Glance `Days` cell and to the `Effort sum` line.
204
+ - **Engineering-only scope** — no approval gate, permission check, stakeholder alignment or decision checklist.
205
+ - **Prose vs structure** — the narrative does not contradict the tables.
206
+ - **No assumed background.** Flag any sentence that only parses if the reader watched the plan being argued: a cited objection, a prior revision, "the reporter's rationale", a decision's provenance. Flag okstra's own vocabulary reaching the page — `task-group`, a `taskType` printed as a status, a task-id nobody outside the run can look up — and any enum printed without being defined on the page.
207
+ - **Written for the executor.** Each step says what to do and what the executor should see; a step that only explains why the plan chose this shape is a finding.
208
+ - **One language.** Table cells and stage titles follow the schedule's prose language; only headings and field labels stay English literals.
209
+
210
+ These last three are the checks no validator can make. Do not accept a `pass` that skipped them.
186
211
  5. **Revise from the first gate after every change.** If either gate requests a change, the lead revises the same draft in place, then starts again at the deterministic `--selection-json` gate. Allow **Max 2 revise cycles** total across both gates.
187
212
  6. **Gate publication.** Only the same draft that passes both gates may be promoted in Step 6. If it still fails after the second revision, do not write the final file; remove the paired staging artifacts and report the residual findings in Korean.
188
213
 
@@ -230,5 +255,5 @@ Reached only after both Step 5.5 gates return `pass`. Promote the verified same
230
255
 
231
256
  - All user-facing messages in Korean; schedule body prose Korean, identifiers/headings/field labels English (template literals).
232
257
  - Use project-relative paths in completion messages.
233
- - Use `<taskType> / <currentPhase>` (not `workStatus`) for each detail `Status`; `workStatus` only filters candidates.
258
+ - Each detail `Status` is derived from `taskType` and `workflow.currentPhase` but written in plain words; `workStatus` only filters candidates and is never printed.
234
259
  - Per-task section Work Breakdown rows are **selected stages**, not free-form items; done stages are summarized, never scheduled forward.
@@ -14,6 +14,14 @@ approved: {{ "true" if frontmatter.approved else "false" }}
14
14
  implementation-option: {{ frontmatter.implementationOption | default("") | yaml_scalar }}
15
15
  schema-version: {{ schemaVersion | yaml_scalar }}
16
16
  ---
17
+ {#
18
+ The spine renders these by hand or leaves them to the human HTML, so the
19
+ closing `md_rest("")` sweep must not repeat them: `frontmatter` and `header`
20
+ appear above, `humanSummary` is the HTML's own narrative, and `verdictCard`
21
+ is a non-authoritative index of `finalVerdict`, which §Final Verdict already
22
+ carries in full.
23
+ -#}
24
+ {{ md_claim("schemaVersion", "meta", "frontmatter", "header", "humanSummary", "verdictCard") }}
17
25
 
18
26
  # {{ frontmatter.title }}
19
27
 
@@ -33,34 +41,90 @@ schema-version: {{ schemaVersion | yaml_scalar }}
33
41
  - Blocking IDs: {{ aiBlockingIds | join(", ") if aiBlockingIds else "none" }}
34
42
  - Next step: {{ finalVerdict.nextStep }}
35
43
 
36
- ### Decision Context
44
+ ## Decision Context
37
45
 
38
- {{ aiDecisionContext | json_block }}
46
+ ### Final Verdict
39
47
 
40
- ### Next Task Contract
48
+ {{ md("finalVerdict", 4) }}
41
49
 
42
- {{ aiRoutingContract | json_block }}
50
+ ### Summary
51
+
52
+ {{ md("summary", 4) }}
53
+
54
+ ### Rationale
55
+
56
+ {{ md("rationale", 4) }}
57
+ {% if md_has("ticketCoverage") %}
58
+
59
+ ### Ticket Coverage
60
+
61
+ {{ md("ticketCoverage", 4) }}
62
+ {% endif %}
63
+
64
+ ## Next Task Contract
65
+
66
+ ### Recommended Next Steps
67
+
68
+ {{ md("recommendedNextSteps", 4) }}
69
+
70
+ ### Follow-up Tasks
71
+
72
+ {{ md("followUpTasks", 4) }}
43
73
 
44
74
  ## Clarification and User Decisions
45
75
 
46
- {{ clarificationItems | json_block }}
76
+ {{ md("clarificationItems", 3) }}
77
+ {% if md_has("clarificationCarryIn") %}
78
+
79
+ ### Clarification Carried In
80
+
81
+ {{ md("clarificationCarryIn", 4) }}
82
+ {% endif %}
47
83
 
48
84
  ## Evidence Ledger
49
85
 
50
- {{ aiEvidenceLedger | json_block }}
86
+ ### Evidence
87
+
88
+ {{ md("evidence", 4) }}
89
+
90
+ ### Missing Information
91
+
92
+ {{ md("missingInformation", 4) }}
93
+
94
+ ### End-State Coverage
95
+
96
+ {{ md("endStateCoverage", 4) }}
97
+ {% if md_has("analysisCommon") %}
98
+
99
+ ### Analysis Common
100
+
101
+ {{ md("analysisCommon", 4) }}
102
+ {% endif %}
51
103
 
52
104
  ## Task Deliverable: {{ aiTaskDeliverableTitle }}
105
+ {% if aiTaskTemplate %}
106
+
107
+ {% include aiTaskTemplate %}
108
+ {% else %}
53
109
 
54
- {{ aiTaskDeliverable | json_block }}
110
+ {{ md_rest(aiTaskProperty, 3) }}
111
+ {% endif %}
55
112
 
56
113
  ## Cross Verification Audit
57
114
 
58
- {{ crossVerification | json_block }}
115
+ {{ md("crossVerification", 3) }}
59
116
 
60
117
  ## Execution Audit
61
118
 
62
- {{ executionStatus | json_block }}
119
+ {{ md("executionStatus", 3) }}
63
120
 
64
121
  ## Token and Cost Audit
65
122
 
66
- {{ tokenUsage | json_block }}
123
+ {{ md("tokenUsage", 3) }}
124
+ {% set trailing = md_rest("", 3) %}
125
+ {% if trailing %}
126
+
127
+ ## Additional Report Data
128
+
129
+ {{ trailing }}
130
+ {% endif %}
@@ -0,0 +1,19 @@
1
+ {#
2
+ Task-body section helper.
3
+
4
+ The report spine renders its own fixed sections unconditionally — an empty
5
+ `Missing Information` there is a positive statement. A task body is
6
+ variable-shaped instead: most of its fields are schema-optional, so a heading
7
+ over `_none_` would be noise rather than news. `section` therefore emits
8
+ nothing at all when the path carries no value.
9
+
10
+ Import `with context`, otherwise the `md` / `md_has` globals the renderer
11
+ binds are not visible inside the macro.
12
+ -#}
13
+ {% macro section(path, title, level=3) -%}
14
+ {% if md_has(path) %}
15
+ {{ "#" * level }} {{ title }}
16
+
17
+ {{ md(path, level + 1) }}
18
+ {% endif %}
19
+ {%- endmacro %}
@@ -0,0 +1,18 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for the phase that plans this change: what is being asked, what
4
+ must not move, what the change reaches, and what the planner needs carried
5
+ forward. Unaffected boundaries sit next to the blast radius because the pair
6
+ is what bounds the change.
7
+ -#}
8
+ {{ s.section("changeImpactAnalysis.changeRequest", "Change Request") }}
9
+ {{ s.section("changeImpactAnalysis.preservedBehaviors", "Preserved Behaviors") }}
10
+ {{ s.section("changeImpactAnalysis.impactItems", "Impact Items") }}
11
+ {{ s.section("changeImpactAnalysis.dependencyBlastRadius", "Dependency Blast Radius") }}
12
+ {{ s.section("changeImpactAnalysis.unaffectedBoundaries", "Unaffected Boundaries") }}
13
+ {{ s.section("changeImpactAnalysis.compatibilityImpact", "Compatibility Impact") }}
14
+ {{ s.section("changeImpactAnalysis.migrationImpact", "Migration Impact") }}
15
+ {{ s.section("changeImpactAnalysis.rollbackImpact", "Rollback Impact") }}
16
+ {{ s.section("changeImpactAnalysis.securityAndPerformanceImpact", "Security and Performance Impact") }}
17
+ {{ s.section("changeImpactAnalysis.planningInputs", "Planning Inputs") }}
18
+ {{ md_rest("changeImpactAnalysis", 3) }}
@@ -0,0 +1,13 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for the agent that fixes this: the symptom as reported, what is
4
+ observably failing, how to reproduce it, the ranked causes with what would
5
+ disprove each, and the next probe to run.
6
+ -#}
7
+ {{ s.section("errorAnalysis.symptomVerbatim", "Symptom Verbatim") }}
8
+ {{ s.section("errorAnalysis.observableFailure", "Observable Failure") }}
9
+ {{ s.section("errorAnalysis.reproduction", "Reproduction") }}
10
+ {{ s.section("errorAnalysis.causeCandidates", "Cause Candidates") }}
11
+ {{ s.section("errorAnalysis.nextDiagnostic", "Next Diagnostic") }}
12
+ {{ s.section("errorAnalysis.routing", "Routing") }}
13
+ {{ md_rest("errorAnalysis", 3) }}
@@ -0,0 +1,13 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for an agent about to change this feature: what the feature is,
4
+ how it runs, the rules it must keep, what it writes, and what already covers
5
+ it in tests.
6
+ -#}
7
+ {{ s.section("featureAnalysis.target", "Target") }}
8
+ {{ s.section("featureAnalysis.flows", "Flows") }}
9
+ {{ s.section("featureAnalysis.domainRules", "Domain Rules") }}
10
+ {{ s.section("featureAnalysis.stateChanges", "State Changes") }}
11
+ {{ s.section("featureAnalysis.externalInteractions", "External Interactions") }}
12
+ {{ s.section("featureAnalysis.testCoverage", "Test Coverage") }}
13
+ {{ md_rest("featureAnalysis", 3) }}
@@ -0,0 +1,13 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for whoever decides whether this ships: what blocks acceptance,
4
+ what proves the rest passed, and what risk survives a pass.
5
+ -#}
6
+ {{ s.section("finalVerification.acceptanceBlockers", "Acceptance Blockers") }}
7
+ {{ s.section("finalVerification.validationEvidence", "Validation Evidence") }}
8
+ {{ s.section("finalVerification.residualRisk", "Residual Risk") }}
9
+ {{ s.section("finalVerification.manualUserTest", "Manual User Test") }}
10
+ {{ s.section("finalVerification.sourceImplementationReport", "Source Implementation Report") }}
11
+ {{ s.section("finalVerification.stageReports", "Stage Reports") }}
12
+ {{ s.section("finalVerification.routingRecommendation", "Routing Recommendation") }}
13
+ {{ md_rest("finalVerification", 3) }}
@@ -0,0 +1,15 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for the agent that executes this plan: what was chosen, the
4
+ stages it is cut into, how to run each one, and how to tell a stage is done.
5
+ Option candidates and the trade-off matrix answer "why this one" and sit
6
+ after that, in the closing sweep.
7
+ -#}
8
+ {{ s.section("implementationPlanning.recommendedOption", "Recommended Option") }}
9
+ {{ s.section("implementationPlanning.stageMap", "Stage Map") }}
10
+ {{ s.section("implementationPlanning.stages", "Stages") }}
11
+ {{ s.section("implementationPlanning.stepwiseExecution", "Stepwise Execution") }}
12
+ {{ s.section("implementationPlanning.validationChecklist", "Validation Checklist") }}
13
+ {{ s.section("implementationPlanning.rollbackStrategy", "Rollback Strategy") }}
14
+ {{ s.section("implementationPlanning.requirementCoverage", "Requirement Coverage") }}
15
+ {{ md_rest("implementationPlanning", 3) }}
@@ -0,0 +1,15 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for the verifier that picks this stage up: what the plan asked
4
+ for, what actually landed, what proves it, and what the implementer knows is
5
+ still open.
6
+ -#}
7
+ {{ s.section("implementation.approvedPlanReference", "Approved Plan Reference") }}
8
+ {{ s.section("implementation.diffSummary", "Diff Summary") }}
9
+ {{ s.section("implementation.commitList", "Commit List") }}
10
+ {{ s.section("implementation.requirementCoverage", "Requirement Coverage") }}
11
+ {{ s.section("implementation.validationEvidence", "Validation Evidence") }}
12
+ {{ s.section("implementation.outOfPlanEdits", "Out-of-Plan Edits") }}
13
+ {{ s.section("implementation.manualUserTest", "Manual User Test") }}
14
+ {{ s.section("implementation.routingRecommendation", "Routing Recommendation") }}
15
+ {{ md_rest("implementation", 3) }}
@@ -0,0 +1,10 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for whoever picks work off this sidetrack: the candidates
4
+ themselves, then the lens coverage that says which angles were actually swept
5
+ and which were not.
6
+ -#}
7
+ {{ s.section("improvementDiscovery.candidates", "Candidates") }}
8
+ {{ s.section("improvementDiscovery.lensCoverage", "Lens Coverage") }}
9
+ {{ s.section("improvementDiscovery.selectionLimit", "Selection Limit") }}
10
+ {{ md_rest("improvementDiscovery", 3) }}
@@ -0,0 +1,15 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for an agent orienting in an unfamiliar repository: the feature
4
+ index and entry points first, because they are how it finds anything, then the
5
+ component map, then where the risk and the coverage gaps are.
6
+ -#}
7
+ {{ s.section("projectAnalysis.featureIndex", "Feature Index") }}
8
+ {{ s.section("projectAnalysis.entryPoints", "Entry Points") }}
9
+ {{ s.section("projectAnalysis.components", "Components") }}
10
+ {{ s.section("projectAnalysis.dataStores", "Data Stores") }}
11
+ {{ s.section("projectAnalysis.externalSystems", "External Systems") }}
12
+ {{ s.section("projectAnalysis.rankedHotspots", "Ranked Hotspots") }}
13
+ {{ s.section("projectAnalysis.qualityCoverage", "Quality Coverage") }}
14
+ {{ s.section("projectAnalysis.scanCoverage", "Scan Coverage") }}
15
+ {{ md_rest("projectAnalysis", 3) }}
@@ -0,0 +1,13 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for whoever picks the release up: what was handed off, what the
4
+ branch and PR ended up as, and whether a merge would conflict.
5
+ -#}
6
+ {{ s.section("releaseHandoff.handoffScope", "Handoff Scope") }}
7
+ {{ s.section("releaseHandoff.userSelections", "User Selections") }}
8
+ {{ s.section("releaseHandoff.featureBranchState", "Feature Branch State") }}
9
+ {{ s.section("releaseHandoff.pullRequestOutcome", "Pull Request Outcome") }}
10
+ {{ s.section("releaseHandoff.mergeConflictProbe", "Merge Conflict Probe") }}
11
+ {{ s.section("releaseHandoff.executedCommands", "Executed Commands") }}
12
+ {{ s.section("releaseHandoff.routingRecommendation", "Routing Recommendation") }}
13
+ {{ md_rest("releaseHandoff", 3) }}
@@ -0,0 +1,15 @@
1
+ {% import "md/macros/sections.md" as s with context -%}
2
+ {#
3
+ Reading order for the phase that plans from these requirements: what the user
4
+ asked verbatim, how the system read it, and which requirements are settled
5
+ versus still open. The verbatim request leads because every later reading is
6
+ judged against it.
7
+ -#}
8
+ {{ s.section("requirementsDiscovery.requestVerbatim", "Request Verbatim") }}
9
+ {{ s.section("requirementsDiscovery.systemInterpretation", "System Interpretation") }}
10
+ {{ s.section("requirementsDiscovery.classification", "Classification") }}
11
+ {{ s.section("requirementsDiscovery.confirmedRequirements", "Confirmed Requirements") }}
12
+ {{ s.section("requirementsDiscovery.unresolvedRequirements", "Unresolved Requirements") }}
13
+ {{ s.section("requirementsDiscovery.rejectionCriteria", "Rejection Criteria") }}
14
+ {{ s.section("requirementsDiscovery.routing", "Routing") }}
15
+ {{ md_rest("requirementsDiscovery", 3) }}