okstra 0.25.1 → 0.27.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 (48) hide show
  1. package/README.kr.md +16 -0
  2. package/README.md +16 -0
  3. package/docs/kr/architecture.md +3 -7
  4. package/docs/kr/cli.md +47 -4
  5. package/docs/kr/performance-improvement-plan-v2.md +23 -0
  6. package/docs/kr/performance-improvement-plan.md +22 -0
  7. package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +254 -0
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/SKILL.md +30 -2
  11. package/runtime/bin/okstra.sh +1 -1
  12. package/runtime/prompts/profiles/_common-contract.md +30 -1
  13. package/runtime/prompts/profiles/error-analysis.md +12 -0
  14. package/runtime/prompts/profiles/implementation-planning.md +23 -0
  15. package/runtime/prompts/profiles/requirements-discovery.md +20 -0
  16. package/runtime/python/lib/okstra/cli.sh +8 -7
  17. package/runtime/python/lib/okstra/globals.sh +3 -1
  18. package/runtime/python/lib/okstra/usage.sh +8 -4
  19. package/runtime/python/okstra_ctl/render.py +35 -0
  20. package/runtime/python/okstra_ctl/run.py +27 -6
  21. package/runtime/python/okstra_ctl/run_context.py +1 -1
  22. package/runtime/python/okstra_ctl/wizard.py +259 -10
  23. package/runtime/python/okstra_token_usage/blocks.py +5 -1
  24. package/runtime/python/okstra_token_usage/claude.py +16 -1
  25. package/runtime/python/okstra_token_usage/collect.py +17 -3
  26. package/runtime/python/okstra_token_usage/pricing.py +159 -24
  27. package/runtime/skills/okstra-brief/SKILL.md +532 -65
  28. package/runtime/skills/okstra-context-loader/SKILL.md +25 -11
  29. package/runtime/skills/okstra-convergence/SKILL.md +235 -8
  30. package/runtime/skills/okstra-history/SKILL.md +68 -37
  31. package/runtime/skills/okstra-logs/SKILL.md +26 -4
  32. package/runtime/skills/okstra-report-finder/SKILL.md +49 -22
  33. package/runtime/skills/okstra-report-writer/SKILL.md +59 -64
  34. package/runtime/skills/okstra-run/SKILL.md +53 -39
  35. package/runtime/skills/okstra-schedule/SKILL.md +51 -20
  36. package/runtime/skills/okstra-setup/SKILL.md +31 -12
  37. package/runtime/skills/okstra-status/SKILL.md +20 -8
  38. package/runtime/skills/okstra-team-contract/SKILL.md +27 -15
  39. package/runtime/skills/okstra-time-summary/SKILL.md +53 -16
  40. package/runtime/templates/reports/final-report.template.md +34 -0
  41. package/runtime/templates/reports/settings.template.json +7 -4
  42. package/runtime/validators/lib/fixtures.sh +10 -2
  43. package/runtime/validators/lib/validate-assets.sh +50 -24
  44. package/runtime/validators/validate-brief.py +385 -0
  45. package/runtime/validators/validate-brief.sh +35 -0
  46. package/runtime/validators/validate-run.py +71 -0
  47. package/runtime/validators/validate-workflow.sh +7 -33
  48. package/src/wizard.mjs +21 -5
@@ -10,7 +10,7 @@ model: opus
10
10
 
11
11
  Generate a consolidated work schedule for all non-done tasks in a given `task-group`. The skill reads each task's `task-manifest.json` and `latestReport`, classifies tasks into phases by priority and risk, and writes a single Markdown plan file under `.project-docs/okstra/tasks/<task-group>/schedule/`.
12
12
 
13
- The default mode is lightweight (single Claude lead synthesis). The `--cross-verify` option triggers the full okstra multi-agent flow on the schedule itself.
13
+ The skill runs as a single Claude lead synthesis (lightweight mode). A `--cross-verify` multi-agent variant was previously sketched here but never specified end-to-end; it has been dropped pre-1.0 and is tracked as a follow-up if multi-agent schedule verification is needed later.
14
14
 
15
15
  ## When to Use
16
16
 
@@ -30,12 +30,12 @@ The default mode is lightweight (single Claude lead synthesis). The `--cross-ver
30
30
 
31
31
  **Explicit command**:
32
32
  - `okstra schedule <task-group>`
33
- - `okstra schedule <task-group> --cross-verify`
34
33
  - `okstra schedule <task-group> --title "<custom title>"`
34
+ - `okstra schedule <task-group> --directive-file <abs-path>` (optional; see "Directive override" below)
35
35
 
36
36
  If `--title` is omitted, derive a default title from `task-group` (e.g. `uploadFont` → `uploadFont — Work Schedule`).
37
37
 
38
- ## Step 0: Verify okstra runtime + project setup
38
+ ## Preflight: Verify okstra runtime + project setup
39
39
 
40
40
  Run before anything else in this skill:
41
41
 
@@ -81,17 +81,19 @@ This skill performs cross-task synthesis (multi-task classification, dependency
81
81
  ### Step 1: Resolve task-group and collect tasks
82
82
 
83
83
  1. Read `.project-docs/okstra/discovery/task-catalog.json`.
84
- 2. Filter entries where `taskGroupPathSegment == <task-group lowercased>` OR `taskGroup == <task-group>` (case-insensitive match).
84
+ 2. **Normalise the user-supplied `<task-group>` argument:** lowercase it, then strip every character that is not `[a-z0-9]` (drop spaces, hyphens, underscores, dots, etc.). Apply the same transform to each entry's `taskGroupPathSegment`. Match when the two normalised forms are equal. This is the single comparison rule — do NOT also fall back to the raw `taskGroup` field.
85
85
  3. If no tasks found, output `해당 task-group을 찾을 수 없습니다.` and stop.
86
86
  4. For each matched task, read `.project-docs/okstra/tasks/<task-group-segment>/<task-id-segment>/task-manifest.json` directly. Catalog data may be stale; the manifest is authoritative.
87
87
  5. **Derive `<project-id>`** for the schedule header: prefer `task-catalog.json`'s top-level `projectId` field if present, otherwise use the first matched manifest's `projectId` field. Do not invent a value.
88
88
 
89
89
  ### Step 2: Filter by workStatus
90
90
 
91
- For each task-manifest:
92
- - If `workStatus` field is missing or empty → treat as `in-progress` (include).
93
- - If `workStatus == "done"` → exclude.
94
- - Otherwise (`todo` / `in-progress` / `blocked`) → include.
91
+ Since the `feat(scripts/render): project workStatus fields into task-catalog entries` change (commit `c44c36b`), `workStatus` is also surfaced directly inside each catalog entry — Step 1 still re-reads each `task-manifest.json` because the manifest is authoritative, but no extra fetch is needed beyond that.
92
+
93
+ For inference rules when `workStatus` is missing or empty, **defer to the inference table in `skills/okstra-status/SKILL.md` ("Step 4 Inferring workStatus")** instead of duplicating it here. Apply that table to derive a working value, then filter:
94
+
95
+ - If the resolved value is `done` (set by the user OR inferred via `currentStatus == "completed"` + terminal next-phase) → exclude.
96
+ - Otherwise (`todo` / `in-progress` / `blocked` / `phase-done` / explicit user value / inferred non-done) → include.
95
97
 
96
98
  If after filtering 0 tasks remain, output:
97
99
  ```
@@ -133,20 +135,35 @@ If the report file does not exist or cannot be parsed:
133
135
  If parsing fails for a specific section:
134
136
  - Mark the entry with `[PARSE-ERROR: <section>]` and continue.
135
137
 
136
- ### Step 4: Mode branching
138
+ ### Step 4: Synthesis
137
139
 
138
- - **Default (lightweight)**: Claude lead synthesizes the schedule directly using collected data. Proceed to Step 5.
139
- - **`--cross-verify`**: Invoke the parent `okstra` skill flow with the schedule synthesis itself as the analysis target. The schedule generation becomes the cross-verified deliverable.
140
+ Claude lead synthesises the schedule directly using collected data. Proceed to Step 5.
140
141
 
141
142
  ### Step 5: Phase classification heuristic
142
143
 
143
- Classify each task into one of three phases based on report data:
144
+ Classify each task into one of three phases based on report data.
145
+
146
+ **`workCategory` accepted values** (de-facto enum, from `scripts/okstra_ctl/worktree.py::_WORK_CATEGORY_PREFIX` plus the `unknown` fallback emitted by `render.py`):
147
+
148
+ | workCategory | Default phase |
149
+ |--------------|---------------|
150
+ | `bugfix` | Phase 1 (when risk is High/Med-High); otherwise Phase 2 |
151
+ | `feature` | Phase 2 |
152
+ | `improvement` | Phase 2 |
153
+ | `refactor` | Phase 3 |
154
+ | `ops` | Phase 3 |
155
+ | `docs` / `doc` | Phase 2 |
156
+ | `unknown` (or unmatched / missing) | **Phase 2**, with a one-line rationale `> _workCategory '<raw-value>' 미정의 — Phase 2로 기본 분류._` at the top of that phase section |
157
+
158
+ Priority overrides category:
159
+
160
+ - **Phase 1 (안정성/Critical)**: priority `P0`, OR `workCategory == "bugfix"` with High / Med-High risk
161
+ - **Phase 2 (개선/기능)**: priority `P1` or `P2`, OR `workCategory in {"feature", "improvement", "docs", "doc"}`, OR fallback per the table above
162
+ - **Phase 3 (확장/아키텍처)**: priority `P3`, OR `workCategory in {"refactor", "ops"}`, OR multi-repo + infrastructure scope
144
163
 
145
- - **Phase 1 (안정성/Critical)**: priority `P0`, OR `workCategory == "bugfix"` with high risk
146
- - **Phase 2 (개선/기능)**: priority `P1` or `P2`, `workCategory in {"improvement", "feature"}`
147
- - **Phase 3 (확장/아키텍처)**: priority `P3`, OR multi-repo + infrastructure scope
164
+ When the classification is genuinely ambiguous after applying the table + priority override, place the task in the closest phase and add a one-line rationale at the top of that phase section.
148
165
 
149
- When the classification is ambiguous, place the task in the closest phase and add a one-line rationale at the top of that phase section.
166
+ > Note: enum codification (turning this de-facto list into a `validators/`-enforced contract) is out of scope for this skill file as a follow-up if needed.
150
167
 
151
168
  ### Step 6: Write the schedule file
152
169
 
@@ -168,7 +185,7 @@ After writing the file, reply briefly:
168
185
  - 포함 task: N개
169
186
  - 제외(done) task: M개
170
187
  - 예상 소요: X.X ~ Y.Y days (Effort 합산)
171
- - 모드: lightweight | cross-verify
188
+ - 모드: lightweight
172
189
  ```
173
190
 
174
191
  ## Audience (READ FIRST — drives everything below)
@@ -312,7 +329,17 @@ When skipping, follow the skip-reason note rule below — but the skip should be
312
329
 
313
330
  #### Directive override (highest priority)
314
331
 
315
- Before applying the heuristics above, **check `instruction-set/analysis-material.md` for a `## Directive` section** (sourced from the `--directive` CLI flag). If that section is present and expresses any directive that affects Gantt rendering — e.g. "render a Gantt even with single XL task", "no Gantt needed" — that directive **overrides the heuristic and the skip-reason rule** for the affected section. When following a Directive override that contradicts the default heuristic, append a one-line note inside the rendered (or skipped) section: `> _Per Directive directive: <verbatim short excerpt>._` so the reader can trace why the section appeared/disappeared against the default. The Directive may also pre-supply day allocations, phase weights, or sub-task decompositions — use those verbatim as bar lengths in the Gantt.
332
+ Before applying the heuristics above, **check the schedule-level directive source** for a `## Directive` section.
333
+
334
+ **Resolution order (first hit wins):**
335
+
336
+ 1. Absolute path passed via the `--directive-file <abs-path>` argument when invoking the skill.
337
+ 2. `<PROJECT_ROOT>/.project-docs/okstra/tasks/<task-group-segment>/schedule/instruction-set/analysis-material.md` (the canonical schedule-level instruction-set location — okstra writes here when a schedule run is dispatched with `--directive`).
338
+ 3. **No directive file** — fall through and apply the default heuristic without any override. This is the normal case; do not warn, do not block.
339
+
340
+ If a directive source is found and contains a `## Directive` section that affects Gantt rendering — e.g. "render a Gantt even with single XL task", "no Gantt needed" — that directive **overrides the heuristic and the skip-reason rule** for the affected section. When following a Directive override that contradicts the default heuristic, append a one-line note inside the rendered (or skipped) section: `> _Per Directive directive: <verbatim short excerpt>._` so the reader can trace why the section appeared/disappeared against the default. The Directive may also pre-supply day allocations, phase weights, or sub-task decompositions — use those verbatim as bar lengths in the Gantt.
341
+
342
+ A directive file that exists but contains no `## Directive` heading is treated as "no directive" — fall through to the heuristic, no warning required.
316
343
 
317
344
  `## Gantt Chart` is the only `##` section that MAY be added beyond the 11 mandatory ones. Any other extra `##` heading is forbidden.
318
345
 
@@ -415,6 +442,10 @@ Required shapes:
415
442
  | Skipping `## Gantt Chart` because the data is "wide range / single task / part-day allocation pending" | Per the render-by-default rule, these are reasons to render an **estimate-tagged** chart, not to skip. Skip is only legitimate when day data literally does not exist (all-XXL or no effort sizing anywhere) |
416
443
  | Rendering Gantt as a mermaid (`` ```mermaid `` / `gantt` block) or any other graph DSL (PlantUML, Graphviz, etc.) | This skill renders ASCII only. Mermaid output is forbidden — use the ASCII Gantt format described above. Validators may reject mermaid blocks under this heading |
417
444
 
445
+ ## Known Validator Gaps (out of scope here)
446
+
447
+ - The ambiguous-classification rationale required by Step 5 (`> _workCategory '<raw>' 미정의 …_` / nearest-phase rationale) is **not** currently enforced by `validators/validate-schedule.py`. The skill is responsible for emitting it; closing the validator gap requires a validator change and is tracked as a follow-up.
448
+
418
449
  ## Self-Validation Before Reporting Completion
419
450
 
420
451
  After writing the file and before printing the completion message in Step 7, you MUST:
@@ -596,13 +627,13 @@ Markdown bullet list of next concrete engineering actions, one item per line: `-
596
627
 
597
628
  | Case | Handling |
598
629
  |------|----------|
599
- | `workStatus` field absent or empty | Treat as `in-progress` include in schedule |
630
+ | `workStatus` field absent or empty | Resolve via the okstra-status inference table; include unless the resolved value is `done` |
600
631
  | Filtered task count is 0 | Emit "모든 task가 done" message; do NOT create a file |
601
632
  | `latestReportPath` missing or unreadable | Tag entry with `[NEEDS-OKSTRA-RUN]`; include manifest metadata only |
602
633
  | Report parsing fails for a specific section | Tag with `[PARSE-ERROR: <section>]`; continue with the rest |
603
634
  | `task-group` argument matches no tasks | Output "해당 task-group을 찾을 수 없습니다." and stop |
604
635
  | Catalog and manifest disagree on `workStatus` | Manifest wins (catalog may be stale) |
605
- | Multiple `task-group` casings | Match case-insensitively; use the manifest's `taskGroupPathSegment` for path output |
636
+ | Multiple `task-group` casings / punctuation variants | Normalise both sides (lowercase + strip non-`[a-z0-9]`) then compare against `taskGroupPathSegment` only. Use the manifest's `taskGroupPathSegment` verbatim for path output. |
606
637
 
607
638
  ## Output Rules
608
639
 
@@ -40,7 +40,8 @@ npx -y okstra@latest install
40
40
  This single command populates everything the user needs:
41
41
 
42
42
  - `~/.okstra/{lib/python, bin, version}` — python + bash runtime
43
- - `~/.claude/skills/<name>/SKILL.md` — all 11 okstra skills (incl. this one)
43
+ - `~/.claude/skills/<name>/SKILL.md` — all okstra skills (canonical count
44
+ in `~/.okstra/installed-skills.json`)
44
45
  - `~/.okstra/installed-skills.json` — manifest for safe uninstall
45
46
 
46
47
  The skill should run this even if `~/.okstra/version` already exists —
@@ -72,10 +73,11 @@ them from the env vars.
72
73
  ## Step 3: Resolve PROJECT_ROOT
73
74
 
74
75
  ```bash
75
- okstra check-project --cwd "$(pwd)"
76
+ PROJECT_ROOT=$(okstra check-project --cwd "$(pwd)" | jq -r '.projectRoot')
76
77
  ```
77
78
 
78
- The JSON includes `projectRoot` on success. On failure (`ok: false`,
79
+ The JSON includes `projectRoot` on success. Bind it into the shell so
80
+ Step 4 onwards can expand `$PROJECT_ROOT`. On failure (`ok: false`,
79
81
  `stage: "resolve"`) ask the user (`AskUserQuestion`, free text) for an
80
82
  absolute project root and rerun with `--cwd <their answer>`.
81
83
 
@@ -96,7 +98,11 @@ so overwriting requires manually deleting the file first.
96
98
  If the file does NOT exist, ask via `AskUserQuestion`:
97
99
 
98
100
  - **Question**: `"Project id for okstra (e.g. INV-1234, my-app, okstra)"`
99
- - **Validate**: slugified must contain at least one alphanumeric character.
101
+ - **Validate**: the answer must be non-empty AND contain at least one
102
+ alphanumeric character. Re-ask on empty input — `okstra setup --yes`
103
+ with no `--project-id` exits 1 with
104
+ `error: --project-id is required (no existing project.json, not a TTY)`,
105
+ so passing the user's empty answer through is a silent failure path.
100
106
 
101
107
  Then create the file:
102
108
 
@@ -104,6 +110,12 @@ Then create the file:
104
110
  okstra setup --yes --project-root "$PROJECT_ROOT" --project-id "$PROJECT_ID"
105
111
  ```
106
112
 
113
+ > After this, **Steps 4.5–4.8 are all optional**. The built-in defaults
114
+ > work for most projects — skip straight to Step 5 (`doctor`) unless the
115
+ > user explicitly asks to customise worktree sync, declare QA commands,
116
+ > or register a PR-body template. Step 4.7 (settings.local.json symlink)
117
+ > is automatic; it is documented for diagnostic purposes only.
118
+
107
119
  ## Step 4.5 (optional): customise worktree sync dirs
108
120
 
109
121
  Each okstra run provisions a task-scoped git worktree under
@@ -114,14 +126,14 @@ every task sees the shared state. The built-in default is
114
126
 
115
127
  To override per-project, add a `worktreeSyncDirs` array to
116
128
  `project.json`. Empty array disables the feature; the field is
117
- preserved across the runtime's auto-upserts (only `projectId`,
118
- `projectRoot`, `createdAt`, `updatedAt` are runtime-owned).
129
+ preserved across the runtime's auto-upserts (see Step 4.6 for the
130
+ canonical list of runtime-owned fields).
119
131
 
120
132
  ```json
121
133
  {
122
134
  "projectId": "...",
123
135
  "projectRoot": "...",
124
- "worktreeSyncDirs": [".project-docs", ".scratch", ".claude", "my-custom-dir"]
136
+ "worktreeSyncDirs": [".project-docs", ".scratch", "graphify-out", ".claude", "my-custom-dir"]
125
137
  }
126
138
  ```
127
139
 
@@ -130,7 +142,7 @@ field → built-in default. Only edit when defaults don't cover the
130
142
  project's working files (e.g. additional cache or local-config dirs
131
143
  that must follow the executor into the worktree).
132
144
 
133
- ## Step 4.7 (optional but recommended): declare project QA commands
145
+ ## Step 4.6 (optional but recommended): declare project QA commands
134
146
 
135
147
  `implementation`-phase verifiers run an independent QA gate over the
136
148
  executor's diff and need a project-wide baseline of check-only
@@ -178,7 +190,7 @@ The field is preserved across the runtime's auto-upserts of
178
190
  `updatedAt` are runtime-owned, so manual edits to `qaCommands`
179
191
  survive every subsequent `okstra setup` / `okstra run` invocation.
180
192
 
181
- ## Step 4.6 (automatic): project-local Claude settings symlink
193
+ ## Step 4.7 (automatic): project-local Claude settings symlink
182
194
 
183
195
  `okstra setup` (and `okstra run` on its first invocation per project)
184
196
  provisions `<PROJECT_ROOT>/.claude/settings.local.json` as a symlink to
@@ -209,7 +221,7 @@ To opt out (advanced): replace the symlink with a regular file. okstra
209
221
  will detect that it is no longer a symlink on its next setup call and
210
222
  back it up as `.bak.<timestamp>` rather than overwriting silently.
211
223
 
212
- ## Step 4.8 (optional): register a project PR body template
224
+ ## Step 4.8 (optional, opt-in): register a project PR body template
213
225
 
214
226
  `release-handoff` fills the PR body from a template. By default it uses the
215
227
  bundled skill template at
@@ -217,6 +229,11 @@ bundled skill template at
217
229
  want their own — e.g. the repo's `.github/PULL_REQUEST_TEMPLATE.md` — to
218
230
  keep PRs consistent with what the team already merges manually.
219
231
 
232
+ **Pre-registration during setup is opt-in.** The same prompt is also
233
+ offered on the first `release-handoff` run in this project, so it is
234
+ safe to defer this step (`나중에`) — most users should skip unless they
235
+ explicitly want to lock the template in now.
236
+
220
237
  Ask the user with `AskUserQuestion` (fixed options, NOT free text — file
221
238
  path entry happens in the follow-up plain text prompt per the
222
239
  okstra-run prompt convention):
@@ -269,6 +286,8 @@ Inform the user with a short summary:
269
286
  |---|---|---|
270
287
  | `command not found: npx` | Node missing | Install node 18+. |
271
288
  | `okstra ensure-installed` keeps reinstalling | `~/.okstra/version` write fails (permissions) | Check `~/.okstra` ownership and writability. |
272
- | `ResolverError: project_id required` | empty answer to Step 4 prompt | Re-ask Step 4. |
273
- | `projectId 불일치` | `project.json` already exists with a different id | Decide which id is canonical; manually edit the file or pick the existing id. |
289
+ | `error: --project-id is required (no existing project.json, not a TTY)` | `okstra setup --yes` invoked without `--project-id`, or with empty answer to Step 4 prompt | Re-ask Step 4 and pass a non-empty id via `--project-id`. |
290
+ | `projectId mismatch` / `projectId 불일치` | `project.json` already exists with a different id | Decide which id is canonical; manually delete `<PROJECT_ROOT>/.project-docs/okstra/project.json` to re-register, or re-run with the existing id. |
291
+ | `EACCES` writing under `.project-docs/okstra/` | directory owned by another user (e.g. created by a previous root-shell run) | `chown -R "$USER" <PROJECT_ROOT>/.project-docs` or delete and let setup recreate. |
292
+ | `warning: failed to provision .claude/settings.local.json symlink` | a non-symlink `.claude/settings.local.json` already exists and the backup-and-replace step failed | Inspect `<PROJECT_ROOT>/.claude/settings.local.json{,.bak.*}`; manually merge project-specific rules, then re-run setup. |
274
293
  | `npx okstra@latest install` succeeds but `doctor` shows FAIL | runtime/{python,bin,skills} sync not yet performed (pre-release package) | Use dev install: clone the repo and run `node bin/okstra install --link <repo>`. |
@@ -40,7 +40,7 @@ parse and reuse it instead of re-resolving in the steps below.
40
40
 
41
41
  ## Step 1: Overall Project Status
42
42
 
43
- To view the overall project status, first read `.project-docs/okstra/discovery/task-catalog.json`.
43
+ To view the overall project status, read `.project-docs/okstra/discovery/task-catalog.json`. The catalog is the authoritative source — every field listed below (including `workStatus`, `workStatusUpdatedAt`, `workStatusNote`) is projected directly from each `task-manifest.json` by `scripts/okstra_ctl/render.py :: render_task_catalog_discovery`. Do NOT re-open individual manifests for the overview.
44
44
 
45
45
  Extract the following fields from each task.
46
46
 
@@ -68,13 +68,19 @@ Sort by:
68
68
 
69
69
  출력 형식:
70
70
 
71
+ The overview table is intentionally narrow so it renders cleanly in a terminal. Only six columns are shown; for any task that needs a closer look (phase state, routing, approval gate, last run status, resume path, etc.) tell the user to run `okstra status <task-key>` for the detail view in Step 2.
72
+
73
+ If `awaitingApproval` is true OR `routingStatus == "pending"`, append a `*` to the `Next` cell as a visual marker and explain the marker once below the table.
74
+
71
75
  ```markdown
72
76
  ## okstra Status — <project-id>
73
77
 
74
- | # | Task Key | Category | Phase | Phase State | Task Status | workStatus | Next | Routing | Approval | Last Run |
75
- |---|----------|----------|-------|-------------|-------------|------------|------|---------|----------|----------|
76
- | 1 | proj:group:id | bugfix | error-analysis | completed | completed | in-progress | implementation-planning | not-applicable | no | completed |
77
- | 2 | proj:group:id2 | feature | requirements-discovery | prepared | instruction-set-generated | done | pending-routing-decision | pending | yes | prepared |
78
+ | # | Task Key | Category | Phase | workStatus | Next |
79
+ |---|----------|----------|-------|------------|------|
80
+ | 1 | proj:group:id | bugfix | error-analysis | in-progress | implementation-planning |
81
+ | 2 | proj:group:id2 | feature | requirements-discovery | done | pending-routing-decision* |
82
+
83
+ `*` = awaiting user approval or pending routing decision. Run `okstra status <task-key>` for details.
78
84
  ```
79
85
 
80
86
  ## Step 2: Specific Task Status
@@ -153,10 +159,12 @@ The status response always includes one of the following options:
153
159
  2. **Restart current phase**
154
160
  - Indicates whether the task can be re-run with the same `task-key` and the current `taskType`.
155
161
  3. **Start next phase**
156
- - If `workflow.nextRecommendedPhase` is one of `error-analysis`, `implementation-planning`, `final-verification`, or `release-handoff`, that phase is proposed as the next candidate for the Okstra run.
162
+ - If `workflow.nextRecommendedPhase` is one of `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, or `release-handoff`, that phase is proposed as the next candidate for the Okstra run.
157
163
  - If `nextRecommendedPhase` is `pending-release-handoff`, the prior `final-verification` run completed but its verdict still has to be inspected before entering `release-handoff` — surface this as a verdict-gated next step and present `release-handoff` as a candidate only when the cited verdict is `accepted`.
158
164
  4. **Need more information**
159
165
  - If `nextRecommendedPhase` is `pending-routing-decision` or `routingStatus` is `pending`, this indicates that additional information is required.
166
+ 5. **Task complete (terminal)**
167
+ - If `nextRecommendedPhase` is `done-or-follow-up`, the task lifecycle has reached its terminal signal. This is **not** a "next phase" — do not propose a new Okstra run. Surface the latest report and ask the user whether any follow-up task should be opened separately.
160
168
 
161
169
  ## Step 4: Update workStatus
162
170
 
@@ -195,7 +203,7 @@ Accepted `<status>` values: `todo`, `in-progress`, `blocked`, `done`.
195
203
  Stop without modifying any file.
196
204
  - If no match → output `<TASK-ID>를 찾을 수 없습니다.` and stop.
197
205
 
198
- 3. **Open the matching `task-manifest.json`** at `.project-docs/okstra/tasks/<task-group-segment>/<task-id-segment>/task-manifest.json`.
206
+ 3. **Open the matching `task-manifest.json`** at `.project-docs/okstra/tasks/<task-group-segment>/<task-id-segment>/task-manifest.json`. Assemble the path from the catalog entry's `taskGroupPathSegment` and `taskIdPathSegment` fields (these are the filesystem-safe segments emitted by the renderer) — NOT from the raw user-supplied `<task-group>` / `<task-id>` strings, which may differ in case or contain characters that were normalized when the manifest was created. As a defensive shortcut, prefer the `taskManifestPath` field directly when present.
199
207
 
200
208
  4. **Update fields at the manifest root**:
201
209
  - `workStatus` ← new status value
@@ -224,7 +232,7 @@ If `workStatus` is missing or empty in any manifest, infer the display value fro
224
232
 
225
233
  | Manifest state | Inferred display |
226
234
  |---|---|
227
- | `currentStatus == "completed"` AND `workflow.nextRecommendedPhase` in (`done-or-follow-up`, empty) | `done` (inferred) |
235
+ | `currentStatus == "completed"` AND `workflow.nextRecommendedPhase == "done-or-follow-up"` | `done` (inferred) |
228
236
  | `currentStatus == "completed"` AND `workflow.currentPhaseState == "completed"` | `phase-done` (inferred) |
229
237
  | `currentStatus == "contract-violated"` OR `workflow.currentPhaseState == "blocked"` | `blocked` (inferred) |
230
238
  | anything else | `in-progress` (default) |
@@ -244,3 +252,7 @@ This skill updates `task-manifest.json` only. `discovery/task-catalog.json` may
244
252
  - If there is no recent report, display `--`.
245
253
  - If a specific task does not exist, explicitly state that it cannot be found based on `task-catalog.json`.
246
254
  - If `awaitingApproval` is true, clearly indicate that the task is awaiting user approval.
255
+
256
+ ## Out-of-Scope Backlog
257
+
258
+ - **Step 0 boilerplate duplication.** The `ensure-installed` + `paths --shell` + `check-project --json` preamble is byte-identical across every user-facing okstra skill. The Claude Code skill framework has no include/snippet mechanism today, so each skill duplicates the block. A future change should either (a) extract the preamble into a single `okstra preflight` subcommand the skill can call in one line, or (b) ship the block as a shared SKILL fragment if the framework gains include support. Not actionable inside this skill alone.
@@ -11,6 +11,8 @@ user-invocable: false
11
11
  - When verifying worker team composition and operational rules
12
12
  - When applying model assignment rules
13
13
 
14
+ **Not applicable to `release-handoff`** — that profile is lead-only and intentionally has no `Required workers:` block (see `prompts/profiles/release-handoff.md`). The worker-dispatch contract in this document does not engage during `release-handoff` runs.
15
+
14
16
  ## Team Structure
15
17
 
16
18
  okstra tasks are always operated using the `Claude lead` + required worker team structure.
@@ -19,24 +21,27 @@ okstra tasks are always operated using the `Claude lead` + required worker team
19
21
 
20
22
  **All analysis workers (Claude / Codex / Gemini) share an identical core responsibility.** Specialization is additive — it lives in optional Section 6 of the worker output, NOT in differentiated core questions. This is intentional: cross-verification only converges if all three workers are answering the same questions against the same brief. Disjoint per-worker scopes produce union-of-perspectives, not triangulation.
21
23
 
22
- | Role | Core responsibility | Specialization lens (Section 6 only) | Default Model | subagent_type | Notes |
23
- |------|------|------|-----------|---------------|------|
24
- | Claude lead | orchestration + convergence supervision + final-report review/approval | — | opus | -- | Does NOT author the final-report file when `Report writer worker` is in the roster |
25
- | Claude worker | Answer every brief question across feasibility, requirement interpretation, hidden assumptions, and alternatives — with file:line evidence | broad reasoning depth, hidden assumptions, execution-risk surfacing | sonnet | claude-worker | `agents/claude-worker.md` |
26
- | Codex worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | implementation realism, code-path implications, edge cases, technical trade-offs | gpt-5.5 | codex-worker | `agents/codex-worker.md` |
27
- | Gemini worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | requirement interpretation, consistency, safety, alternative viewpoints | auto | gemini-worker | `agents/gemini-worker.md` |
28
- | Report writer worker | **Authors** the final-report file in Phase 6. NOT an analysis worker. | — | opus | report-writer-worker | `agents/report-writer-worker.md`. Excluded from Phase 4/5 and convergence |
24
+ | Role | Core responsibility | Specialization lens (Section 6 only) | subagent_type | Notes |
25
+ |------|------|------|---------------|------|
26
+ | Claude lead | orchestration + convergence supervision + final-report review/approval | — | -- | Does NOT author the final-report file when `Report writer worker` is in the roster |
27
+ | Claude worker | Answer every brief question across feasibility, requirement interpretation, hidden assumptions, and alternatives — with file:line evidence | broad reasoning depth, hidden assumptions, execution-risk surfacing | claude-worker | `agents/claude-worker.md` |
28
+ | Codex worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | implementation realism, code-path implications, edge cases, technical trade-offs | codex-worker | `agents/codex-worker.md` |
29
+ | Gemini worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | requirement interpretation, consistency, safety, alternative viewpoints | gemini-worker | `agents/gemini-worker.md` |
30
+ | Report writer worker | **Authors** the final-report file in Phase 6. NOT an analysis worker. | — | report-writer-worker | `agents/report-writer-worker.md`. Excluded from Phase 4/5 and convergence |
31
+
32
+ **Model assignment has no default.** The model for every role comes from `resultContract.requiredWorkerRoles[*].modelExecutionValue` in `task-manifest.json` (and lead model metadata). There is no per-role hard-coded fallback — see "Model Assignment Rules" below.
29
33
 
30
34
  **Dispatch-prompt invariant (BLOCKING).** Lead's dispatch prompt body for Claude / Codex / Gemini workers MUST be byte-identical except for the role label and any wrapper-specific path headers (e.g. `**Worktree:**`, `**Errors sidecar path:**`). Lead MUST NOT bias the brief by inserting per-worker emphasis sentences ("you focus on X") into the body. Bias-by-prompt reproduces the historical failure mode where Claude commented only on assumptions, Codex only on code paths, and Gemini only on requirements — leaving convergence with nothing to converge on.
31
35
 
32
36
  ### Model Assignment Rules
33
37
 
34
- 1. If there is an explicit assignment in `resultContract.requiredWorkerRoles` in `task-manifest.json` and in the lead model metadata, that is the canonical assignment.
35
- 2. If there is no explicit assignment, use the default model above.
36
- 3. If `modelExecutionValue` differs from `model`, use `modelExecutionValue` during execution.
38
+ 1. `resultContract.requiredWorkerRoles` in `task-manifest.json` (and the lead model metadata) is the canonical source. There is no role-level fallback — a missing assignment is a manifest defect, not a license to invent one.
39
+ 2. If `modelExecutionValue` differs from `model`, use `modelExecutionValue` during execution.
37
40
 
38
41
  ### Dynamic Worker Role Determination
39
42
 
43
+ **Roster canonical-source rule.** The profile's `Required workers:` block (in `prompts/profiles/<phase>.md`) is the **static roster definition** — the set of roles legal for that phase. `resultContract.requiredWorkerRoles` in `task-manifest.json` is the **per-run instance** — the actual roster materialized for this run, after recommendation, user selection, and any post-recommendation overrides. **On conflict, the task-manifest wins** — it is what the run was actually launched with, and what lead must dispatch against.
44
+
40
45
  Only workers selected from `recommendedWorkers` in `task-manifest.json` and `resultContract.requiredWorkerRoles` become required roles.
41
46
 
42
47
  - If one worker is selected: "`<role>` is the required worker role for this run."
@@ -251,7 +256,11 @@ The same frontmatter contract applies to the `Report writer worker`'s final-repo
251
256
 
252
257
  A successful worker result must include the following sections in this exact order, beneath the frontmatter block:
253
258
 
254
- 0. **Reading Confirmation** — one short line per input file (`task-brief.md`, `analysis-profile.md`, `analysis-material.md` if present, `reference-expectations.md`, `clarification-response.md` if a carry-in was provided, `final-report-template.md`) stating that the worker read it end-to-end. Each line takes the form `- Read <file-name> end-to-end (<line-count> lines).`. If a file was skipped or only partially read, the worker MUST NOT produce sections 1–5; instead it records a `tool-failure` in the errors sidecar and stops. This section exists specifically to counteract the common failure mode where workers skim long inputs because they share structure with the file the run will eventually write into.
259
+ 0. **Reading Confirmation** — one short line per input file stating that the worker read it end-to-end. Each line takes the form `- Read <file-name> end-to-end (<line-count> lines).`. The enumerated files are audience-scoped they MUST match the recipient's row in the "Audience-scoped enumeration" table above:
260
+ - **Claude / Codex / Gemini analysis workers**: `task-brief.md`, `analysis-profile.md`, `analysis-material.md` (if present), `reference-expectations.md`, `clarification-response.md` (if a carry-in was provided). Analysis workers MUST NOT include `final-report-template.md` — it is not in their `[Required reading]` block.
261
+ - **Report writer worker (Phase 6)**: all of the above **plus** `final-report-template.md`.
262
+
263
+ If a file was skipped or only partially read, the worker MUST NOT produce sections 1–5; instead it records a `tool-failure` in the errors sidecar and stops. This section exists specifically to counteract the common failure mode where workers skim long inputs because they share structure with the file the run will eventually write into.
255
264
  1. Findings
256
265
  2. Missing Information or Assumptions
257
266
  3. Safe or Reasonable Areas
@@ -341,7 +350,7 @@ without proceeding — this is the contractual replacement for the previous
341
350
  empty run-level error logs in production.
342
351
 
343
352
  - `cli-failure` events are recorded by the wrapper subagent itself (Codex / Gemini), but **directly to the run-level error log** via `okstra-error-log.py append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
344
- - **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-gemini-exec.sh` accept four positional arguments: `<project-root> <model> <prompt-path> [<worktree-path>]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise. For codex it becomes `--add-dir <worktree>` (sandbox write access); for gemini it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt.
353
+ - **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-gemini-exec.sh` accept four required positional arguments plus an optional fifth `<role>`: `<project-root> <model> <prompt-path> <worktree-path> [<role>]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise. For codex it becomes `--add-dir <worktree>` (sandbox write access); for gemini it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is the trace-pane label suffix (e.g. `codex-<role>-trace`); always pass the literal string `worker` so the dispatch is self-describing (the wrapper defaults to `worker` if omitted).
345
354
  - **Background dispatch + polling contract (Codex / Gemini wrappers).** Both wrapper subagents MUST dispatch `okstra-codex-exec.sh` / `okstra-gemini-exec.sh` via `Bash(run_in_background: true)` and poll with `BashOutput(bash_id)` until the shell reports `status == "completed"`, capped at 30 minutes (1800s) of wall-clock elapsed time. `BashOutput` itself is the wait primitive — call it back-to-back; do NOT insert a standalone `sleep` between polls. The Claude Code harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps inside until-loops to work around the block. Workers that hit the contract bug must NOT self-recover with `until ...; do sleep 2; done` wrappers — that path violates the harness anti-circumvention rule, even though it superficially "works". The legacy "single foreground `Bash` with 120000ms timeout" rule, and the subsequent "60-second cadence with `sleep 60` between polls" rule, are both retired. The current rule applies in **every phase** (analysis runs typically complete in 1–2 `BashOutput` calls, so there is no regression for short jobs). Recording responsibilities:
346
355
  - Successful completion: return the wrapper's accumulated stdout from the final `BashOutput`. No log entry.
347
356
  - Non-zero `exit_code` reported by `BashOutput`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
@@ -356,7 +365,7 @@ empty run-level error logs in production.
356
365
  2. Re-verification workers follow a constrained response format (verdict + brief explanation).
357
366
  3. Workers cannot vote on their own findings (only verify other workers’ work).
358
367
  4. The `report writer worker` does not participate in re-verification voting. It is responsible only for generating the final report.
359
- 5. The Claude lead determines the semantic equivalence of findings (this is not delegated to workers).
368
+ 5. Division of labor: the Claude lead performs **finding-to-finding matching** (deciding which worker-A finding maps to which worker-B finding for cross-review) and mediates the round protocol; **workers cast the AGREE / DISAGREE / SUPPLEMENT votes** that determine consensus. The lead does NOT vote on substance and does NOT collapse worker disagreements by fiat — disagreements flow into the `contested` / `partial-consensus` classifications defined in `skills/okstra-convergence/SKILL.md`.
360
369
  6. Batch processing is performed with one spawn per worker per round (not one spawn per finding).
361
370
  7. These rules do not apply if Convergence is disabled.
362
371
 
@@ -376,10 +385,13 @@ Every worker result file under `worker-results/` must begin with a standardized
376
385
  # <Role> Analysis — <task-key>
377
386
 
378
387
  **Task:** <task-type>
388
+ **Target:** <path or scope> <!-- OPTIONAL: include when the run is scoped to a specific file/module -->
379
389
  **Date:** <YYYY-MM-DD>
380
390
  **Model:** <Role>, <AI model>
381
391
  ```
382
392
 
393
+ The `Target:` line is optional. Include it when the run is scoped to a specific path or module; omit it when the run spans the whole project. When included, place it between `Task:` and `Date:` as shown.
394
+
383
395
  Examples:
384
396
 
385
397
  ```markdown
@@ -422,12 +434,12 @@ Token usage is collected from agent session transcripts after the run, NOT from
422
434
  At the **start of Phase 7** (persistence), run the helper script with the path to this run's `team-state.json`:
423
435
 
424
436
  ```bash
425
- python3 scripts/okstra-token-usage.py \
437
+ python3 "$HOME/.okstra/lib/python/okstra-token-usage.py" \
426
438
  <runDirectoryPath>/state/team-state-<task-type>-<seq>.json \
427
439
  --write --summary
428
440
  ```
429
441
 
430
- (Use the absolute path to `scripts/okstra-token-usage.py` it lives in `Okstra/scripts/`.)
442
+ The script is installed at `$HOME/.okstra/lib/python/okstra-token-usage.py` by `okstra install`. The previous repo-relative path (`scripts/okstra-token-usage.py`) only exists in a working clone of the okstra repo and is not appropriate for end-user-deployed runs.
431
443
 
432
444
  The script reads:
433
445
  - `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` for the lead and every Claude-side worker (Claude worker, Report writer worker, plus the Claude wrappers around Codex/Gemini workers). Sessions are discovered by `teamName: okstra-<task-id>`, lead is identified by `lead.sessionId`, and other workers are identified by `agentName` (e.g. `claude-worker`, `codex-worker`, `gemini-worker`, `report-writer`).
@@ -6,7 +6,7 @@ user-invocable: false
6
6
 
7
7
  # OKSTRA Time Summary
8
8
 
9
- Aggregate elapsed work time for a given task, grouped by **task type** and broken down by **worker** (lead, intake, claude-worker, codex-worker, gemini-worker, report-writer).
9
+ Aggregate elapsed work time for a given task, grouped by **task type** and broken down by **worker** (lead, claude, codex, gemini, report-writer).
10
10
 
11
11
  ## When to Use
12
12
 
@@ -19,7 +19,7 @@ Aggregate elapsed work time for a given task, grouped by **task type** and broke
19
19
  Two sources, both already collected by `okstra`:
20
20
 
21
21
  1. `.project-docs/okstra/tasks/<task-group>/<task-id>/history/timeline.json`
22
- — lists every run with `runTimestamp`, `taskType`, `status`, `teamStatePath`.
22
+ — lists every run with `runTimestamp`, `taskType`, `status`, `teamStatePath`, and `taskRootPath`. Both path fields may be either project-root-relative or task-root-relative depending on which version of `render.py` wrote the manifest.
23
23
  2. Each run's `.../runs/<task-type>/state/team-state-<suffix>.json`
24
24
  — populated by `scripts/okstra-token-usage.py` at Phase 7. Contains:
25
25
  - `leadUsage.{startedAt, endedAt, durationMs}`
@@ -64,12 +64,20 @@ If `task-catalog.json` is missing, respond: "No okstra history found. Run `scrip
64
64
 
65
65
  For each entry in `timeline.json`'s `runs` array:
66
66
 
67
- 1. Read the `team-state` file at `teamStatePath` (relative to the project root).
67
+ 1. Resolve the `team-state` file using a two-step lookup:
68
+ a. First try `<projectRoot>/<teamStatePath>`.
69
+ b. If that file does not exist, fall back to `<projectRoot>/<taskRootPath>/<teamStatePath>` (the manifest's `taskRootPath` field is the task-root relative to project root; `teamStatePath` written by `render.py` is task-root-relative in many runs).
70
+ Either path satisfies the lookup. If neither resolves to an existing file, treat the run as `unavailable`.
68
71
  2. Extract:
69
72
  - `taskType` from the timeline entry (authoritative).
70
73
  - `leadUsage.durationMs` and `leadUsage.{startedAt,endedAt}`.
71
74
  - For each `worker` in `workers[]`: `workerId`, `agent`, `usage.durationMs`.
72
- 3. If the team-state file is missing, or all `durationMs` values are 0/absent, record the run under `unavailable` with its `runTimestamp` and `taskType`.
75
+ Read defensively. `usage` (and `leadUsage`) may be:
76
+ - a normal `usage_block` with `durationMs >= 0`,
77
+ - a `na_block` with `{"source": "unavailable", "durationMs": 0, "note": ...}` when Phase 7 collection failed,
78
+ - missing entirely (older team-state files), or `None`.
79
+ Always normalize via `(block or {}).get("durationMs", 0) or 0`, and treat a `source == "unavailable"` block as zero contribution.
80
+ 3. If the team-state file is missing, or every `durationMs` for the run is `0`/absent (i.e. `leadUsage` and every `workers[].usage` are zero or unavailable), record the run under `unavailable` with its `runTimestamp` and `taskType`.
73
81
 
74
82
  ## Step 3: Aggregate
75
83
 
@@ -82,20 +90,50 @@ For each distinct `taskType` across runs:
82
90
  | Column | Computation |
83
91
  |--------|-------------|
84
92
  | `Runs` | count of runs of that task type that contributed any duration |
85
- | `Total` | sum of (lead + all workers) across those runs |
93
+ | `CPU sum` | sum of (lead + all workers) across those runs — see note below |
86
94
  | `Lead` | sum of `leadUsage.durationMs` |
87
95
  | `Workers` | sum of all `workers[].usage.durationMs` |
88
96
 
89
97
  Add a final `Grand total` row.
90
98
 
99
+ **Note on `CPU sum` vs wall-clock**: workers run as children of the lead session, so the lead's `durationMs` window OVERLAPS its workers' windows. `CPU sum = Lead + Workers` is therefore an additive CPU-style sum, not the wall-clock elapsed time the user actually waited.
100
+
101
+ Worked example for one run with three concurrent workers:
102
+
103
+ ```
104
+ lead [================================] durationMs = 1800000 (30:00)
105
+ claude [============] durationMs = 720000 (12:00)
106
+ codex [==============] durationMs = 840000 (14:00)
107
+ gemini [========] durationMs = 480000 (08:00)
108
+ ```
109
+
110
+ - `CPU sum` for the run = `1800000 + 720000 + 840000 + 480000` = `3840000` (`01:04:00`)
111
+ - Wall-clock for the run = `max(endedAt) − min(startedAt)` ≈ `30:00`
112
+
113
+ Always report `CPU sum` in the by-task-type table. If the user explicitly asks for wall-clock, compute it per run as `max(leadUsage.endedAt, max(workers[].usage.endedAt)) − min(leadUsage.startedAt, min(workers[].usage.startedAt))` and surface it separately — never silently substitute it for `CPU sum`.
114
+
91
115
  ### B. Per worker breakdown (per task type)
92
116
 
93
- For each task type, list one row per `workerId` actually present, plus `lead` and (if non-zero) `intake`. Aggregate `durationMs` across all runs of that task type.
117
+ For each task type, list one row per `workerId` actually present, plus `lead`. Aggregate `durationMs` across all runs of that task type.
94
118
 
95
119
  | Worker | Runs | Total | Avg/run |
96
120
  |--------|------|-------|---------|
97
121
 
98
- Use the `workerId` from team-state (e.g. `claude`, `codex`, `gemini`, `report-writer`). When the same `workerId` ran with different `agent` values across runs, append the agent in parentheses (`claude (claude)`, `codex (codex)`).
122
+ - `Runs` denominator = number of runs of this task type in which this worker recorded a **nonzero** `durationMs`. A run where the worker's block was `na_block`, missing, or `0` does NOT count.
123
+ - If `Runs == 0` for a worker, **omit the row entirely** rather than dividing by zero.
124
+ - `Avg/run = Total / Runs` (integer ms, then format to `HH:MM:SS`).
125
+
126
+ Use the `workerId` from team-state. The valid worker enum is `lead, claude, codex, gemini, report-writer`.
127
+
128
+ Display rule for `workerId` vs `agent`:
129
+ - If every run of this task type used `agent == workerId` for this row, display the bare `workerId` (e.g. `claude`).
130
+ - If `agent` differs from `workerId` (e.g. a `claude` worker slot ran with `agent == "sonnet-eval"`), display `workerId (agent)` — and if multiple distinct agents were used across runs, comma-join them: `claude (sonnet-eval, opus-eval)`.
131
+
132
+ Never write `claude (claude)` — the parenthesized agent is shown only when it adds information.
133
+
134
+ ### Timestamp parsing
135
+
136
+ When you need `startedAt` / `endedAt` (e.g. for wall-clock or chronological sort within a task type), normalize the ISO-8601 string before comparing: replace a trailing `Z` with `+00:00`, accept explicit offsets as-is, and parse via `datetime.fromisoformat(s.replace("Z", "+00:00"))`. Strings without an offset are assumed UTC. Mixed-form comparisons must be done as `datetime` objects, never as raw strings.
99
137
 
100
138
  ## Step 4: Format output
101
139
 
@@ -110,19 +148,20 @@ Use the `workerId` from team-state (e.g. `claude`, `codex`, `gemini`, `report-wr
110
148
 
111
149
  ### By task type
112
150
 
113
- | Task type | Runs | Total | Lead | Intake | Workers |
114
- |------------------------|------|-----------|----------|----------|----------|
115
- | requirements-discovery | 2 | 00:34:12 | 00:12:08 | 00:01:00 | 00:21:04 |
116
- | error-analysis | 1 | 00:18:45 | 00:08:11 | -- | 00:10:34 |
117
- | implementation | 3 | 02:11:09 | 00:45:30 | -- | 01:25:39 |
118
- | **Grand total** | 6 | **03:04:06** | 01:05:49 | 00:01:00 | 01:57:17 |
151
+ | Task type | Runs | CPU sum | Lead | Workers |
152
+ |------------------------|------|-----------|----------|----------|
153
+ | requirements-discovery | 2 | 00:33:12 | 00:12:08 | 00:21:04 |
154
+ | error-analysis | 1 | 00:18:45 | 00:08:11 | 00:10:34 |
155
+ | implementation | 3 | 02:11:09 | 00:45:30 | 01:25:39 |
156
+ | **Grand total** | 6 | **03:03:06** | 01:05:49 | 01:57:17 |
157
+
158
+ `CPU sum` adds the lead window to each worker window even though they overlap; it is not a wall-clock total.
119
159
 
120
160
  ### Per worker — requirements-discovery
121
161
 
122
162
  | Worker | Runs | Total | Avg/run |
123
163
  |----------------|------|----------|----------|
124
164
  | lead | 2 | 00:12:08 | 00:06:04 |
125
- | intake | 1 | 00:01:00 | 00:01:00 |
126
165
  | claude | 2 | 00:09:12 | 00:04:36 |
127
166
  | codex | 2 | 00:07:40 | 00:03:50 |
128
167
  | gemini | 2 | 00:03:12 | 00:01:36 |
@@ -134,8 +173,6 @@ Use the `workerId` from team-state (e.g. `claude`, `codex`, `gemini`, `report-wr
134
173
  > Unavailable: 1 run (implementation / 2026-04-30_03-03-48) — team-state has no durationMs (Phase 7 not reached)
135
174
  ```
136
175
 
137
- If the `Intake` column is all zero across every task type, omit that column entirely.
138
-
139
176
  ## Output Rules
140
177
 
141
178
  - Always render durations as `HH:MM:SS`; never raw milliseconds.