okstra 0.97.1 → 0.98.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 (74) hide show
  1. package/README.kr.md +4 -3
  2. package/README.md +4 -3
  3. package/docs/kr/architecture.md +2 -2
  4. package/docs/kr/cli.md +2 -0
  5. package/docs/kr/container.md +122 -0
  6. package/docs/project-structure-overview.md +12 -2
  7. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +714 -0
  8. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +125 -0
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/antigravity-worker.md +2 -18
  12. package/runtime/agents/workers/claude-worker.md +10 -33
  13. package/runtime/agents/workers/codex-worker.md +2 -18
  14. package/runtime/agents/workers/report-writer-worker.md +2 -10
  15. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +5 -2
  16. package/runtime/prompts/profiles/_clarification-recommendation.md +1 -0
  17. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  18. package/runtime/prompts/profiles/_common-contract.md +3 -3
  19. package/runtime/prompts/profiles/_coverage-critic.md +17 -0
  20. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  21. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  22. package/runtime/prompts/profiles/_implementation-self-check.md +2 -1
  23. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  24. package/runtime/prompts/profiles/_stage-discipline.md +5 -8
  25. package/runtime/prompts/profiles/error-analysis.md +5 -5
  26. package/runtime/prompts/profiles/final-verification.md +4 -4
  27. package/runtime/prompts/profiles/implementation-planning.md +7 -7
  28. package/runtime/prompts/profiles/implementation.md +5 -5
  29. package/runtime/prompts/profiles/improvement-discovery.md +6 -6
  30. package/runtime/prompts/profiles/release-handoff.md +3 -3
  31. package/runtime/prompts/profiles/requirements-discovery.md +4 -4
  32. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  33. package/runtime/python/okstra_ctl/__init__.py +4 -1
  34. package/runtime/python/okstra_ctl/container.py +970 -0
  35. package/runtime/python/okstra_ctl/container_registry.py +93 -0
  36. package/runtime/python/okstra_ctl/error_zip.py +4 -4
  37. package/runtime/python/okstra_ctl/handoff.py +7 -1
  38. package/runtime/python/okstra_ctl/ids.py +40 -1
  39. package/runtime/python/okstra_ctl/listing.py +3 -5
  40. package/runtime/python/okstra_ctl/log_report.py +102 -0
  41. package/runtime/python/okstra_ctl/pane_reclaim.py +5 -4
  42. package/runtime/python/okstra_ctl/paths.py +40 -0
  43. package/runtime/python/okstra_ctl/plan_run_root.py +78 -0
  44. package/runtime/python/okstra_ctl/reconcile.py +4 -2
  45. package/runtime/python/okstra_ctl/resolve_task_key.py +54 -0
  46. package/runtime/python/okstra_ctl/run.py +48 -30
  47. package/runtime/python/okstra_ctl/stage_integrate.py +8 -2
  48. package/runtime/python/okstra_ctl/stage_targets.py +79 -0
  49. package/runtime/python/okstra_ctl/time_report.py +200 -0
  50. package/runtime/python/okstra_ctl/tmux.py +67 -0
  51. package/runtime/python/okstra_ctl/wizard.py +35 -20
  52. package/runtime/python/okstra_project/__init__.py +2 -0
  53. package/runtime/python/okstra_project/state.py +50 -2
  54. package/runtime/skills/okstra-brief/SKILL.md +17 -7
  55. package/runtime/skills/okstra-container/SKILL.md +169 -0
  56. package/runtime/skills/okstra-inspect/SKILL.md +64 -178
  57. package/runtime/skills/okstra-memory/SKILL.md +8 -6
  58. package/runtime/skills/okstra-run/SKILL.md +4 -4
  59. package/runtime/skills/okstra-schedule/SKILL.md +9 -16
  60. package/runtime/skills/okstra-setup/SKILL.md +10 -9
  61. package/runtime/templates/reports/brief.template.md +1 -1
  62. package/runtime/templates/reports/final-report.template.md +1 -1
  63. package/runtime/templates/reports/settings.template.json +3 -1
  64. package/runtime/validators/validate-implementation-plan-stages.py +11 -3
  65. package/src/cli-registry.mjs +28 -0
  66. package/src/commands/inspect/container.mjs +27 -0
  67. package/src/commands/inspect/log-report.mjs +26 -0
  68. package/src/commands/inspect/resolve-task-key.mjs +26 -0
  69. package/src/commands/inspect/task-list.mjs +26 -15
  70. package/src/commands/inspect/time-report.mjs +26 -0
  71. package/src/commands/lifecycle/check-project.mjs +7 -1
  72. package/src/commands/lifecycle/doctor.mjs +16 -0
  73. package/src/lib/skill-catalog.mjs +1 -0
  74. package/runtime/agents/TODO.md +0 -226
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: okstra-inspect
3
3
  description: |
4
- Use for any read-side okstra inspection or status mutation. Single skill dispatches by sub-command to nine facets — status, history, report, time, logs, cost, errors, error-zip, recap. Trigger words include "okstra status", "task status", "current phase", "next phase", "okstra status set", "okstra mark", "<task-id> done|in-progress|진행중|완료", "okstra history", "past runs", "re-run", "resume", "list tasks", "find report", "show report for", "read the okstra report", "continue from report", "작업 시간", "소요 시간", "time summary", "duration", "elapsed", "얼마나 걸렸", "시간 분석", "okstra logs", "로그 현황", "로그 파일", "log size", "log status", "로그 정리", "log cleanup", "okstra context-cost", "context cost", "context-cost", "컨텍스트 비용", "읽기 비용", "산출물 비용", "okstra errors", "error report", "에러 리포트", "에러 보고", "에러 모아줘", "okstra error-zip", "에러 zip", "에러 환류", "에러 번들", "cross-project 에러", "okstra recap", "recap", "작업 요약", "이 task 요약", "전후 요약", "이 작업 설명해줘", "task 질문", "실패 로그 정리".
4
+ Use for any read-side okstra inspection or status mutation. Trigger words include "okstra status", "task status", "current phase", "next phase", "okstra status set", "okstra mark", "<task-id> done|in-progress|진행중|완료", "okstra history", "past runs", "resume", "re-run", "list tasks", "find report", "show report for", "read the okstra report", "continue from report", "작업 시간", "소요 시간", "time summary", "okstra logs", "로그 현황", "log cleanup", "okstra context-cost", "context cost", "컨텍스트 비용", "okstra errors", "error report", "에러 리포트", "okstra error-zip", "에러 zip", "에러 환류", "cross-project 에러", "okstra recap", "recap", "<task-id> 요약", "실패 로그 정리".
5
5
  ---
6
6
 
7
7
  # OKSTRA Inspect
@@ -72,7 +72,7 @@ Read `.okstra/discovery/task-catalog.json`. The catalog is the authoritative sou
72
72
 
73
73
  Sort by `updatedAt` desc, then `taskKey`.
74
74
 
75
- 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>` (or `/okstra-inspect status <task-key>`) for the detail view in `status.2`.
75
+ 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-inspect status <task-key>` for the detail view in `status.2`.
76
76
 
77
77
  If `awaitingApproval` is true OR `routingStatus == "pending"`, append a `*` to the `Next` cell and explain the marker once below the table.
78
78
 
@@ -84,7 +84,7 @@ If `awaitingApproval` is true OR `routingStatus == "pending"`, append a `*` to t
84
84
  | 1 | proj:group:id | bugfix | error-analysis | in-progress | implementation-planning |
85
85
  | 2 | proj:group:id2 | feature | requirements-discovery | done | pending-routing-decision* |
86
86
 
87
- `*` = awaiting user approval or pending routing decision. Run `okstra status <task-key>` for details.
87
+ `*` = awaiting user approval or pending routing decision. Run `/okstra-inspect status <task-key>` for details.
88
88
  ```
89
89
 
90
90
  ### status.2 — Specific task status
@@ -150,7 +150,7 @@ Recognize requests to change a task's `workStatus` and update the corresponding
150
150
 
151
151
  Natural language: "DEV-6827 done으로 바꿔줘", "PROD-1623을 blocked로 표시", "DEV-9047 진행중", "Mark DEV-6827 as done".
152
152
 
153
- Explicit command:
153
+ Explicit phrasing (recognized user utterances — these are **not** okstra CLI subcommands; `status.4` edits the manifest directly):
154
154
  - `okstra status set <task-id> <status>`
155
155
  - `okstra status set <task-group> <task-id> <status>` (disambiguation)
156
156
  - `okstra mark <task-id> <status>`
@@ -161,9 +161,11 @@ Accepted `<status>` values: `todo`, `in-progress`, `blocked`, `done`.
161
161
  **Procedure:**
162
162
 
163
163
  1. **Validate status value.** If not in the enum, output an error and the allowed values list. Do not modify any file.
164
- 2. **Look up the task** in `.okstra/discovery/task-catalog.json` by `task-id` (case-insensitive on both `task-id` and `task-group`).
165
- - If the user provided `<task-group>` explicitly, scope the lookup to that group (case-insensitive).
166
- - Single match → proceed. Multiple matches across different groups → list and ask user to retry with explicit form (`okstra status set <task-group> <TASK-ID> <status>`). No match → `<TASK-ID>를 찾을 수 없습니다.` and stop.
164
+ 2. **Resolve the task** with the shared resolver (pass `--task-group` when the user gave one — it scopes the match case-insensitively):
165
+ ```bash
166
+ okstra resolve-task-key <task-id> [--task-group <task-group>] --project-root <projectRoot> --json
167
+ ```
168
+ Branch on `matches[]`: single → proceed. Multiple → list candidates and ask the user to retry with the explicit form (`okstra status set <task-group> <TASK-ID> <status>`). None → `<TASK-ID>를 찾을 수 없습니다.` and stop.
167
169
  3. **Open the matching `task-manifest.json`** at `.okstra/tasks/<task-group-segment>/<task-id-segment>/task-manifest.json`. Assemble the path from the catalog entry's `taskGroupPathSegment` and `taskIdPathSegment` fields (filesystem-safe segments emitted by the renderer) — NOT from the raw user-supplied strings, which may differ in case or contain characters normalized when the manifest was created. Prefer the `taskManifestPath` field directly when present.
168
170
  4. **Update fields at the manifest root:**
169
171
  - `workStatus` ← new status value
@@ -265,7 +267,7 @@ Builds a fresh run — new run-seq, new manifest, new report — using parameter
265
267
  4. **`taskType: implementation` only — resolve `--base-ref`:** base ref is NOT in the run-manifest; it lives in the worktree registry at `~/.okstra/worktrees/registry.json`. If a worktree is already registered, existing branch & base are reused — omit `--base-ref` unless the user explicitly wants a different starting point. If no worktree is registered (cleaned up), `--base-ref` is mandatory — ask the user before running.
266
268
  5. Display the assembled command:
267
269
  ```bash
268
- okstra.sh \
270
+ ~/.okstra/bin/okstra.sh \
269
271
  --project-id <project-id> \
270
272
  --task-group <task-group> \
271
273
  --task-id <task-id> \
@@ -294,7 +296,7 @@ Trigger phrases: "find report", "show report for", "read the okstra report", "co
294
296
 
295
297
  task-key format: `<project-id>:<task-group>:<task-id>`.
296
298
 
297
- **Normalization:** task-key matching is lowercase. Disk segments are slugified (lowercase + non-alphanumeric runs → `-`) per `scripts/lib/okstra/interactive.sh:147`. Catalog lookup is case-insensitive; file path assembly uses slugified segments.
299
+ **Normalization:** task-key matching is lowercase. Disk segments are slugified (lowercase + non-alphanumeric runs → `-`) per `scripts/okstra_ctl/ids.py:88` (`slugify_task_segment`, the SSOT; `interactive.sh` consumes it via import). Catalog lookup is case-insensitive; file path assembly uses slugified segments.
298
300
 
299
301
  Lookup methods (in priority order):
300
302
 
@@ -321,11 +323,7 @@ D. **Specific task-type (fallback):** `latestReportPath` is task-type-agnostic.
321
323
  After reading the report, surface follow-up options:
322
324
 
323
325
  1. **구현 진행:** based on the report's "권장 다음 단계" section.
324
- 2. **추가 검증:** new okstra run with the same task-key:
325
- ```bash
326
- scripts/okstra.sh --task-key <task-key> --task-type <task-type>
327
- ```
328
- For richer options (base-ref, workers, render-only), use the `history` sub-command.
326
+ 2. **추가 검증:** to launch a new okstra run with the same task-key, assemble the command through the `history` sub-command — it offers the full option set (base-ref, workers, render-only) and renders a host-correct invocation.
329
327
  3. **관련 task 확인:** if the report references related task-keys, fetch their reports too.
330
328
 
331
329
  ### report — Output template
@@ -349,100 +347,34 @@ After reading the report, surface follow-up options:
349
347
 
350
348
  Trigger phrases: "작업 시간", "소요 시간", "time summary", "duration", "elapsed", "얼마나 걸렸", "시간 분석".
351
349
 
352
- Aggregate elapsed work time for a given task, grouped by **task type** and broken down by **worker** (lead, claude, codex, antigravity, report-writer).
353
-
354
- **Data sources** (both collected by okstra):
355
-
356
- 1. `.okstra/tasks/<task-group>/<task-id>/history/timeline.json` — `runs` array with `runTimestamp`, `taskType`, `status`, `teamStatePath`. `teamStatePath` is project-root-relative (written by `render.py` via `_rel(project_root, ...)`).
357
- 2. Each run's `.../runs/<task-type>/state/team-state-<suffix>.json` — populated by `scripts/okstra-token-usage.py` at Phase 7. Contains `leadUsage.{startedAt, endedAt, durationMs}` and `workers[].{workerId, agent, usage.{startedAt, endedAt, durationMs}}`.
358
-
359
- If a run never reached Phase 7, its `team-state` lacks `durationMs`. Mark such runs as `unavailable` rather than guessing.
360
-
361
- ### time.1 — Resolve task-id → timeline path
362
-
363
- 1. If the user gave a full `task-key`, use it directly.
364
- 2. Otherwise read `.okstra/discovery/task-catalog.json` and find the entry whose `taskId` matches.
365
- 3. Multiple matches → list candidates (`taskKey`, `taskType`, `updatedAt`) and ask the user to pick.
366
- 4. Read `historyTimelinePath` from the chosen entry.
367
-
368
- If `task-catalog.json` is missing: `No okstra history found. Run /okstra-run first.`
369
-
370
- ### time.2 — Walk runs and collect durations
371
-
372
- For each entry in `timeline.json`'s `runs` array:
350
+ Aggregate elapsed work time for a task, grouped by **task type** and broken down by **worker** (lead + each worker). The `time-report` CLI reads both data sources (`history/timeline.json` runs + each run's `team-state-*.json` usage blocks) and does **all** aggregation — cross-run sums, CPU-sum vs wall-clock, timestamp parsing, unavailable detection. You only resolve the task-key, call it, and render. **Never recompute durations by hand.**
373
351
 
374
- 1. Resolve the `team-state` file at `<projectRoot>/<teamStatePath>` — `teamStatePath` is project-root-relative. If `teamStatePath` is empty or the file does not exist on disk, treat the run as `unavailable`.
375
- 2. Extract `taskType` from the timeline entry (authoritative), `leadUsage.durationMs` and `leadUsage.{startedAt,endedAt}`, and for each `worker` in `workers[]`: `workerId`, `agent`, `usage.durationMs`. Read defensively — `usage` (and `leadUsage`) may be:
376
- - a normal `usage_block` with `durationMs >= 0`,
377
- - a `na_block` with `{"source": "unavailable", "durationMs": 0, "note": ...}` when Phase 7 collection failed,
378
- - missing entirely (older team-state files), or `None`.
379
- Normalize via `(block or {}).get("durationMs", 0) or 0`, and treat `source == "unavailable"` as zero contribution.
380
- 3. If the team-state file is missing, or every `durationMs` for the run is `0`/absent, record the run under `unavailable` with its `runTimestamp` and `taskType`.
352
+ ### time.1 Resolve the task-key
381
353
 
382
- ### time.3 Aggregate
354
+ Same standard as `cost.1`: full task-key → use directly; bare task-id → `okstra resolve-task-key <task-id> --project-root <projectRoot> --json` and branch on `matches[]` (0 = not found, 1 = use, N = 3-option picker). If no okstra history exists, say `No okstra history found. Run /okstra-run first.`
383
355
 
384
- **A. Per task-type summary:**
356
+ ### time.2 Fetch aggregated data
385
357
 
386
- | Column | Computation |
387
- |--------|-------------|
388
- | `Runs` | count of runs of that task type that contributed any duration |
389
- | `CPU sum` | sum of (lead + all workers) across those runs |
390
- | `Lead` | sum of `leadUsage.durationMs` |
391
- | `Workers` | sum of all `workers[].usage.durationMs` |
392
-
393
- Add a final `Grand total` row.
394
-
395
- **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 an additive CPU-style sum, not the wall-clock elapsed time the user actually waited.
396
-
397
- Worked example for one run with three concurrent workers:
398
-
399
- ```
400
- lead [================================] durationMs = 1800000 (30:00)
401
- claude [============] durationMs = 720000 (12:00)
402
- codex [==============] durationMs = 840000 (14:00)
403
- antigravity [========] durationMs = 480000 (08:00)
358
+ ```bash
359
+ okstra time-report <task-key> --project-root <projectRoot> --json
404
360
  ```
405
361
 
406
- - `CPU sum` for the run = `1800000 + 720000 + 840000 + 480000` = `3840000` (`01:04:00`)
407
- - Wall-clock for the run = `max(endedAt) min(startedAt)` `30:00`
408
-
409
- 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.
410
-
411
- **B. Per worker breakdown (per task type):**
412
-
413
- For each task type, list one row per `workerId` actually present, plus `lead`. Aggregate `durationMs` across all runs of that task type.
414
-
415
- | Worker | Runs | Total | Avg/run |
416
- |--------|------|-------|---------|
417
-
418
- - `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.
419
- - If `Runs == 0` for a worker, **omit the row entirely** rather than dividing by zero.
420
- - `Avg/run = Total / Runs` (integer ms, then format to `HH:MM:SS`).
421
-
422
- Use the `workerId` from team-state. Valid enum: `lead, claude, codex, antigravity, report-writer`.
362
+ Returns (all durations are **raw milliseconds**):
363
+ - `byTaskType[]` `{taskType, runs, leadMs, workersMs, cpuSumMs}`, plus `grandTotal`
364
+ - `perWorker` — `{<taskType>: [{workerId, agents[], runs, totalMs, avgMs}]}`; only workers with a nonzero run appear, and `agents[]` lists agent labels that differ from `workerId`
365
+ - `perRunWallClock[]` `{runTimestamp, taskType, wallClockMs}` (max `endedAt` − min `startedAt` per run)
366
+ - `phaseTimelines[]` — `{runTimestamp, taskType, phases:[{phase, firstAt, wallMsToNext}]}`
367
+ - `unavailable[]` `{runTimestamp, taskType, reason}` for runs with no Phase-7 durations (never summed into totals)
423
368
 
424
- Display rule for `workerId` vs `agent`:
425
- - If every run of this task type used `agent == workerId` for this row, display the bare `workerId`.
426
- - If `agent` differs (e.g. a `claude` worker slot ran with `agent == "sonnet-eval"`), display `workerId (agent)`. Multiple distinct agents across runs → comma-join: `claude (sonnet-eval, opus-eval)`.
369
+ ### time.3 Render
427
370
 
428
- Never write `claude (claude)` the parenthesized agent is shown only when it adds information.
371
+ Convert every `*Ms` to `HH:MM:SS` (zero-pad; never show raw ms). Task types in `byTaskType` are already in chronological (first-appearance) order.
429
372
 
430
- **C. Per-run phase breakdown (when `phaseTimeline` is present):**
431
-
432
- team-state written by collector v0.73 carries a `phaseTimeline` block the Phase 7 collector reconstructs lead operating-phase boundaries from the lead session's `PROGRESS: phase-*` checkpoint lines (`{source: "lead-progress-markers", phases: [{phase, firstAt, lastAt, markerCount, wallMsToNext}]}`).
433
-
434
- - When the user asks for a per-phase / per-run breakdown ("단계별", "phase별", "어느 단계가 오래"), render one table per requested run: `| Phase | Start | Wall to next |` using `firstAt` and `wallMsToNext`.
435
- - `wallMsToNext` is `null` on the last phase (the run-end signal is not a marker) — render `--`.
436
- - `phases: []` means the lead emitted no markers (run predates the contract, or markers were skipped) — say "phase timeline 측정 불가(마커 없음)" rather than estimating from other fields.
437
- - Older team-state files have no `phaseTimeline` key at all — omit the section silently.
438
-
439
- **Timestamp parsing:** for `startedAt` / `endedAt`, normalize ISO-8601: replace trailing `Z` with `+00:00`, accept explicit offsets as-is, 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 raw strings.
440
-
441
- ### time.4 — Format output
442
-
443
- - Convert `durationMs` to `HH:MM:SS` (zero-pad). Example: `7384000ms` → `02:03:04`.
444
- - Sort task types by order of first appearance in the timeline (chronological, not alphabetical).
445
- - If any runs were `unavailable`, append a final note listing them with reason.
373
+ - **By task type** `| Task type | Runs | CPU sum | Lead | Workers |` from `byTaskType`, plus a `grandTotal` row. `CPU sum` (= Lead + Workers) overlaps because workers run inside the lead's window — it is *not* wall-clock. Surface wall-clock only when the user explicitly asks, from `perRunWallClock`.
374
+ - **Per worker** (per task type) — `| Worker | Runs | Total | Avg/run |` from `perWorker`. Render the worker as bare `workerId` when `agents` is empty, else `workerId (agent1, agent2)`.
375
+ - **Phase breakdown** only when the user asks ("단계별"/"phase별"/"어느 단계가 오래"): one table per run from `phaseTimelines`: `| Phase | Start | Wall to next |` using `firstAt`/`wallMsToNext` (`null` `--`). A run with empty `phases` → "phase timeline 측정 불가(마커 없음)".
376
+ - If `unavailable[]` is non-empty, append a trailing note listing each run with its reason. Never fold them into totals.
377
+ - Show the resolved `<task-key>` in the heading.
446
378
 
447
379
  ```markdown
448
380
  ## Time summary — <task-key>
@@ -452,41 +384,17 @@ team-state written by collector ≥ v0.73 carries a `phaseTimeline` block — th
452
384
  | Task type | Runs | CPU sum | Lead | Workers |
453
385
  |------------------------|------|-----------|----------|----------|
454
386
  | requirements-discovery | 2 | 00:33:12 | 00:12:08 | 00:21:04 |
455
- | error-analysis | 1 | 00:18:45 | 00:08:11 | 00:10:34 |
456
- | implementation | 3 | 02:11:09 | 00:45:30 | 01:25:39 |
457
387
  | **Grand total** | 6 | **03:03:06** | 01:05:49 | 01:57:17 |
458
388
 
459
- `CPU sum` adds the lead window to each worker window even though they overlap; it is not a wall-clock total.
460
-
461
389
  ### Per worker — requirements-discovery
462
390
 
463
- | Worker | Runs | Total | Avg/run |
464
- |----------------|------|----------|----------|
465
- | lead | 2 | 00:12:08 | 00:06:04 |
466
- | claude | 2 | 00:09:12 | 00:04:36 |
467
- | codex | 2 | 00:07:40 | 00:03:50 |
468
- | antigravity | 2 | 00:03:12 | 00:01:36 |
469
- | report-writer | 2 | 00:01:00 | 00:00:30 |
470
-
471
- > Unavailable: 1 run (implementation / 2026-04-30_03-03-48) — team-state has no durationMs (Phase 7 not reached)
472
-
473
- ### Phase breakdown — requirements-discovery (run 002)
474
-
475
- | Phase | Start (UTC) | Wall to next |
476
- |------------------------|------------------|--------------|
477
- | phase-1-intake | 2026-06-11 09:00 | 00:04:00 |
478
- | phase-2-prompts | 2026-06-11 09:04 | 00:02:10 |
479
- | phase-4-dispatch | 2026-06-11 09:06 | 00:18:30 |
480
- | phase-5.5-convergence | 2026-06-11 09:25 | 00:21:00 |
481
- | phase-6-synthesis | 2026-06-11 09:46 | 00:15:00 |
482
- | phase-7-persist | 2026-06-11 10:01 | -- |
483
- ```
391
+ | Worker | Runs | Total | Avg/run |
392
+ |----------------------|------|----------|----------|
393
+ | lead | 2 | 00:12:08 | 00:06:04 |
394
+ | claude (sonnet-eval) | 2 | 00:09:12 | 00:04:36 |
484
395
 
485
- **Rules:**
486
- - Always render durations as `HH:MM:SS`; never raw milliseconds.
487
- - Never invent or estimate `durationMs`. Missing → `--`.
488
- - Never sum across `unavailable` runs into the totals — those are reported only in the trailing note.
489
- - Show the resolved `<task-key>` in the heading so the user can confirm disambiguation.
396
+ > Unavailable: 1 run (implementation / 2026-04-30_03-03-48) — Phase 7 not reached
397
+ ```
490
398
 
491
399
  ---
492
400
 
@@ -512,13 +420,16 @@ If the user gives a full task-key, run:
512
420
  okstra context-cost <task-key> --project-root <projectRoot>
513
421
  ```
514
422
 
515
- If the user gives only a task id:
423
+ If the user gives only a task id, resolve it to a task-key with the shared resolver (SSOT — `scripts/okstra_ctl/resolve_task_key.py` / `okstra_project.resolve_task_id`):
516
424
 
517
- 1. Read `.okstra/discovery/task-catalog.json`.
518
- 2. Match `taskId` case-insensitively.
519
- 3. Single match → use its `taskKey`.
520
- 4. Multiple matches → list candidates and ask the user to retry with a full task-key.
521
- 5. No match report that the task cannot be found.
425
+ ```bash
426
+ okstra resolve-task-key <task-id> --project-root <projectRoot> --json
427
+ ```
428
+
429
+ Branch on the stdout JSON `matches[]` this **standard 0/1/N rule** is referenced by `errors`/`recap` below:
430
+ - **0개** → report the task cannot be found. Do not guess.
431
+ - **1개** → use that entry's `taskKey`.
432
+ - **N개** → list the candidate `taskKey`s (with `updatedAt`) and ask via a 3-option picker (1~2 추천 + `직접 입력`), then use the chosen `taskKey`.
522
433
 
523
434
  If the user asks generally ("컨텍스트 비용 보여줘") and does not name a task:
524
435
 
@@ -536,24 +447,14 @@ okstra context-cost <resolved-target> --project-root <projectRoot>
536
447
 
537
448
  Do not re-count files manually unless the CLI fails and the user explicitly asks for manual fallback.
538
449
 
539
- ### cost.3 — Summarize output
450
+ ### cost.3 — Render
540
451
 
541
- Parse the JSON and report these fields:
452
+ Render the CLI JSON into the cost.4 template. Each row's Files / Size / ~Tokens come from the matching output block — `totals` (Task bundle, Current run, Legacy timestamp), `instructionSet`, `leadPhase1`, `analysisWorker`, `reportWriter`, `skillAssets`; their exact field names are in the CLI output (do not re-list them here). Format bytes as raw + rounded KB/MB.
542
453
 
543
- | Field | Source |
544
- |---|---|
545
- | Task bundle | `totals.taskFileCount`, `totals.taskBytes` |
546
- | Current run | `totals.currentRunFileCount`, `totals.currentRunBytes`, `currentRunPath` |
547
- | Legacy timestamp artifacts | `totals.legacyTimestampFileCount` |
548
- | Instruction set | `instructionSet.fileCount`, `instructionSet.bytes`, `instructionSet.analysisPacketBytes`, `instructionSet.legacyTaskPacketBytes` |
549
- | Lead Phase 1 | `leadPhase1.mode`, `leadPhase1.fileCount`, `leadPhase1.bytes`, `leadPhase1.estimatedTokens` |
550
- | Analysis worker | `analysisWorker.mode`, `analysisWorker.fileCount`, `analysisWorker.bytesPerWorker`, `analysisWorker.estimatedTokensPerWorker`, `analysisWorker.legacyFullContractBytesPerWorker`, `analysisWorker.estimatedPacketModeBytesPerWorker`, `analysisWorker.estimatedReductionPercent` |
551
- | Report writer | `reportWriter.fileCount`, `reportWriter.bytes`, `reportWriter.estimatedTokens` |
552
- | Skill assets (hot path) | `skillAssets.fileCount`, `skillAssets.bytes`, `skillAssets.estimatedTokens`, top entries of `skillAssets.files[]` |
553
-
554
- Format bytes as both raw bytes and rounded KB/MB where useful. Use `analysisWorker.estimatedReductionPercent` for the worker-input reduction. Do not recompute it from `bytesPerWorker` when `analysisWorker.mode == "analysis-packet-primary"` because `bytesPerWorker` is already the packet-primary cost.
555
-
556
- `estimatedTokens*` fields are a static heuristic (~4 ASCII chars/token, non-ASCII ≈ 1 token/char) for ranking instruction surfaces — actual billable cost is the token-usage collector's domain; never present these as billing numbers. `skillAssets` measures the per-run hot-path instruction assets loaded OUTSIDE the task bundle (lifecycle skill bodies + worker agent specs, installed copy preferred) — the prompt-diet target list, sorted by size descending.
454
+ Field-specific notes (these affect interpretation, not just display):
455
+ - Analysis worker uses per-worker fields (`bytesPerWorker`, `estimatedTokensPerWorker`). Use `analysisWorker.estimatedReductionPercent` directly for the worker-input reduction — do **not** recompute it from `bytesPerWorker` when `mode == "analysis-packet-primary"` (that value is already the packet-primary cost).
456
+ - `estimatedTokens*` are a static ranking heuristic (~4 ASCII chars/token, non-ASCII ≈ 1 token/char), never billing numbers — actual cost is the token-usage collector's domain.
457
+ - `skillAssets` = the per-run hot-path instruction assets loaded OUTSIDE the task bundle (skill bodies + agent specs), sorted by size — the prompt-diet target.
557
458
 
558
459
  ### cost.4 — Output template
559
460
 
@@ -611,41 +512,26 @@ The log is truncated at each dispatch (`: > "$log_path"`) — only the latest ru
611
512
 
612
513
  ### logs.1 — Inventory
613
514
 
614
- Construct the logs root by appending `/.okstra/tasks` to the literal `projectRoot` value parsed in Step 0; paste as a literal absolute path in place of `<LOGS_ROOT>` below (no shell variables, no `$(...)`):
615
-
616
515
  ```bash
617
- find <LOGS_ROOT> -type f -path '*/runs/*/prompts/*.log' \
618
- -printf '%s\t%T@\t%p\n' 2>/dev/null \
619
- | sort -k1,1nr
516
+ okstra log-report --project-root <projectRoot> --json
620
517
  ```
621
518
 
622
- Columns: `size_bytes | mtime_epoch | path`.
623
-
624
- On macOS, `find -printf` is unavailable. Fall back to `-exec stat`:
519
+ Scans `<projectRoot>/.okstra/tasks/**/runs/*/prompts/*.log` and parses task/phase/worker/seq from each path. Returns (sizes are **raw bytes**, mtimes **epoch seconds**):
520
+ - `topLargest[]` — `{path, sizeBytes, mtimeEpoch, taskKey, taskGroup, taskId, phase, worker, seq}`, size desc (widen with `--top <N>`)
521
+ - `perTask[]` `{taskKey, fileCount, totalBytes, oldestEpoch, newestEpoch}`, total-size desc
522
+ - `totals` — `{fileCount, totalBytes, taskCount}`
625
523
 
626
- ```bash
627
- find <LOGS_ROOT> -type f -path '*/runs/*/prompts/*.log' -exec stat -f '%z%t%m%t%N' {} + 2>/dev/null | sort -k1,1nr
628
- ```
629
-
630
- If empty, report `No wrapper log files found under <projectRoot>` and exit.
524
+ If `totals.fileCount` is 0, report `No wrapper log files found under <projectRoot>` and stop.
631
525
 
632
526
  ### logs.2 — Summary tables
633
527
 
634
- **Table A Top 20 largest logs:**
635
-
636
- | # | Task | Phase | Worker | Seq | Size | Age | Path |
637
- |---|------|-------|--------|-----|------|-----|------|
638
-
639
- Parse fields from the path: task-group / task-id from the `tasks/<task-group>/<task-id>/` segment; phase from `runs/<phase>/`; worker from filename prefix before `-worker-prompt-` (the wrapper names files `<worker>-worker-prompt-<phase>-<seq>.log`); seq from filename suffix (last 3-digit segment). Format sizes as KB/MB. Format age as `Nd` (days) or `Nh` (hours) from `mtime` relative to "now".
640
-
641
- **Table B — Per-task totals:**
528
+ Render from the CLI output (format bytes → KB/MB; epoch → `Nd`/`Nh` relative to now):
642
529
 
643
- | Task Key | Files | Total Size | Oldest | Newest |
644
- |----------|-------|-----------:|--------|--------|
530
+ **Table A — Top largest logs** (from `topLargest`): `| # | Task | Phase | Worker | Seq | Size | Age | Path |`.
645
531
 
646
- Sort by total size desc. "Task Key" = `<project-id>:<task-group>:<task-id>` for consistency with other sub-commands.
532
+ **Table B Per-task totals** (from `perTask`): `| Task Key | Files | Total Size | Oldest | Newest |`.
647
533
 
648
- **Footer:** `Total: N files, X.X MB across M tasks under <PROJECT_ROOT>`.
534
+ **Footer:** `Total: <fileCount> files, <totalBytes→MB> across <taskCount> tasks under <PROJECT_ROOT>`.
649
535
 
650
536
  ### logs.3 — Suggested cleanup commands
651
537
 
@@ -707,7 +593,7 @@ Aggregate a task's okstra-run error logs (`runs/*/logs/errors-*.jsonl`, lead-obs
707
593
  Accepted target forms (same as `cost`):
708
594
 
709
595
  1. Full task-key: `<project-id>:<task-group>:<task-id>`.
710
- 2. Task id only — read `.okstra/discovery/task-catalog.json`, match `taskId` case-insensitively (multiple list and ask; none report not found).
596
+ 2. Task id only — resolve via `okstra resolve-task-key <task-id> --project-root <projectRoot> --json` and branch on `matches[]` using the standard 0/1/N rule from `cost.1` (0 = not found, 1 = use, N = picker).
711
597
  3. Task root path.
712
598
 
713
599
  ### errors.2 — Run the renderer
@@ -810,7 +696,7 @@ task-id 하나에 쌓인 `.okstra` 산출물 위에서 (a) 처리 전/후 요약
810
696
 
811
697
  ### recap.1 — Resolve target
812
698
 
813
- `cost` / `errors` 와 동일한 3형식: ① full task-key, ② task-id 만(→ `.okstra/discovery/task-catalog.json` `taskId` 대소문자 무시 매칭; 복수면 나열·질문, 없으면 not-found), ③ task-root path.
699
+ `cost` / `errors` 와 동일한 3형식: ① full task-key, ② task-id 만(→ `okstra resolve-task-key <task-id> --project-root <projectRoot> --json`; `cost.1` 표준 0/1/N 분기), ③ task-root path.
814
700
 
815
701
  ### recap.2 — Assemble the before/after summary
816
702
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: okstra-memory
3
- description: Use when the user wants to preserve, remember, store, recall, search, or archive AI/human conversation notes in okstra's global Memory Book. Trigger words include "okstra 에 정리해서 보관해", "memory-book", "기억해둬", "대화 저장", "정리해서 저장", "remember this", "store this conversation", "save this decision".
3
+ description: Use when the user wants to preserve, remember, store, recall, search, or archive AI/human conversation notes in okstra's global Memory Book. Trigger words include "okstra 에 정리해서 보관해", "memory-book", "기억해둬", "대화 저장", "정리해서 저장", "remember this", "store this conversation", "save this decision", "memory-book 검색", "저장한 메모 찾아줘", "recall a saved decision".
4
4
  ---
5
5
 
6
6
  # okstra-memory
@@ -58,15 +58,18 @@ searching so the rest of the skill can scope to it.
58
58
  ```
59
59
 
60
60
  2. Present a 3-option picker (most-used existing group, next existing group,
61
- then always `직접 입력`). If `groups` is empty, recommend `private` as the
62
- first option. Use `private` for the user's personal context.
61
+ then always `직접 입력`). For a personal note, recommend `private` as the
62
+ first option. If `groups` is empty or the user makes no selection, the
63
+ default group is `global` (the CLI default in `memory.mjs`).
63
64
  3. Carry the chosen group name into every `add` (`--project-group <name>`) and
64
65
  into scoped `search`/`list` (`--project-group <name>`) below. Omit the flag
65
66
  only when the user explicitly wants a cross-group search.
66
67
 
67
68
  ## Store current conversation
68
69
 
69
- 1. Extract only durable memory from the conversation:
70
+ 1. Extract only durable memory from the conversation and tag each with a
71
+ `--type`. The authoritative `--type` value list is the `okstra memory --help`
72
+ output read in Step 0; the categories below mirror it:
70
73
  - decision
71
74
  - preference
72
75
  - requirement
@@ -89,8 +92,7 @@ okstra memory add --content "<summary markdown>" --title "<short title>" --type
89
92
  ```
90
93
 
91
94
  Use repeated `--tag` / `--project` flags when needed. Omit `--project` when no
92
- project is clearly related. `--project-group` is the group chosen in Step 1
93
- (default `global` if the user truly wants it ungrouped).
95
+ project is clearly related. `--project-group` is the group chosen in Step 1.
94
96
 
95
97
  ## Search / read / archive
96
98
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: okstra-run
3
- description: Use when the user wants to start an okstra task (cross-verification run) directly from the current Claude Code session — without spawning a new claude process. Equivalent in effect to `okstra.sh --task-type ...` but driven through interactive prompts. Trigger words include "okstra run", "okstra start", "start okstra", "begin okstra task", "run okstra in this session", "okstra here".
3
+ description: Use when the user wants to start an okstra task (cross-verification run) directly from the current Claude Code session — without spawning a new claude process. Equivalent in effect to `okstra.sh --task-type ...` but driven through interactive prompts. Trigger words include "okstra run", "okstra start", "start okstra", "begin okstra task", "run okstra in this session", "okstra here", "이어서 진행", "다음 단계 실행", "이 세션에서 okstra 시작".
4
4
  ---
5
5
 
6
6
  # OKSTRA Run (in-session)
@@ -39,7 +39,7 @@ Every non-terminal `next` carries a `progress` object (`done` / `aborted` omit i
39
39
  "current": { "step": "approved_plan", "kind": "text", "label": "..." } }
40
40
  ```
41
41
 
42
- On `ok: false`, re-prompt with the same `current.step` using the error message. The wizard never advances on validation failure; the user retries the same step.
42
+ On `ok: false`, re-prompt with the same `current.step` using the error message. The wizard never advances on validation failure; the user retries the same step. **`current` may be `null`** when the current step itself cannot render (e.g. the approved plan's Stage Map is corrupt) — that case is terminal: show `error` and stop, the user must fix the plan file before retrying. Never re-prompt off a `null` `current`.
43
43
 
44
44
  The wizard tells you *which UI to use* via `kind` (and the optional `multi` flag on `pick`):
45
45
 
@@ -52,7 +52,7 @@ The wizard tells you *which UI to use* via `kind` (and the optional `multi` flag
52
52
 
53
53
  The final `confirm` step is a normal `pick` step with three options — `Proceed` / `Edit` / `중단`(abort) — and is rendered the same way (no special handling). `Edit` rewinds to any earlier step (including `base-ref`); `중단` terminally cancels the wizard. The branch/worktree decision the run will actually use (for `implementation`, the **stage worktree** — not the task-key directory) is folded into the Step 4 confirmation summary block as a `worktree` line, so there is no separate branch-confirm prompt.
54
54
 
55
- Never invent additional questions. Never reorder. **Never drop, hide, or merge a `pick` / `pick_group` option** — render every `options[]` entry as its own selectable `AskUserQuestion` choice, including entries that carry a `(default)` / `(recommended)` suffix. Do NOT collapse a multi-option pick into a "recommended + 직접 입력 / Other" shortlist: the wizard's `options[]` array IS the complete, authoritative choice set. Example: the `executor` step always emits `claude` / `codex` / `antigravity`show all three, never just `claude`. The run-prompt recommendation rule (1–2 추천 + 직접 입력) applies ONLY to prompts this skill authors itself (e.g. the conformance-waiver picker), never to wizard-provided `options[]`. Never use `AskUserQuestion` for `text` prompts — the wizard explicitly chose `text` to avoid the picker-Other re-render lag.
55
+ Never invent additional questions. Never reorder. **Never drop, hide, or merge a `pick` / `pick_group` option** — render every `options[]` entry as its own selectable `AskUserQuestion` choice, including entries that carry a `(default)` / `(recommended)` suffix. Do NOT collapse a multi-option pick into a "recommended + 직접 입력 / Other" shortlist: the wizard's `options[]` array IS the complete, authoritative choice set. Example: if a pick's `options[]` carries N entries, render all N as selectable choices never abbreviate a multi-option step down to one recommended value. The run-prompt recommendation rule (1–2 추천 + 직접 입력) applies ONLY to prompts this skill authors itself (e.g. the conformance-waiver picker), never to wizard-provided `options[]`. Never use `AskUserQuestion` for `text` prompts — the wizard explicitly chose `text` to avoid the picker-Other re-render lag.
56
56
 
57
57
  ## Step 1: Verify okstra runtime + project setup
58
58
 
@@ -116,7 +116,7 @@ Repeat until `next.kind == "done"` (or `"aborted"` — terminal cancel, see "How
116
116
  **Escaping rule**: if the literal answer contains `"`, escape each occurrence as `\"` inside the double-quoted argument. Empty values must still be `--answer ""` — the flag itself is mandatory, even when the value is empty.
117
117
  3. **Handle result**:
118
118
  - `ok: true` → echo `result.echo` to the user on one short line, then loop with `result.next`.
119
- - `ok: false` → show `result.error` to the user verbatim, then loop with `result.current` (re-prompt the same step).
119
+ - `ok: false` → show `result.error` to the user verbatim, then loop with `result.current` (re-prompt the same step). If `result.current` is `null`, do not loop — the current step cannot render (e.g. corrupt Stage Map); surface the error and stop so the user fixes the plan.
120
120
 
121
121
  That is the entire interactive flow. The wizard handles:
122
122
 
@@ -52,7 +52,7 @@ Subsequent `okstra <subcmd>` calls self-bootstrap their Python path, so this ski
52
52
 
53
53
  ## Process Procedure
54
54
 
55
- ### Step 0: Verify model (HARD GATE)
55
+ ### Step 0: Verify model (권장 게이트)
56
56
 
57
57
  This skill performs cross-task synthesis (multi-task classification, dependency reasoning, phase placement, Gantt/timeline assembly) which benefits substantially from Opus-class reasoning. The frontmatter `model: opus` field above instructs supporting Claude Code harness versions to switch automatically; if the harness ignores it, this gate catches the case explicitly.
58
58
 
@@ -64,7 +64,8 @@ This skill performs cross-task synthesis (multi-task classification, dependency
64
64
  /model opus 로 전환 후 다시 호출하시거나, 'sonnet으로 진행' 이라고 명시하시면 그대로 실행합니다.
65
65
  ```
66
66
  4. If the user explicitly insists on the lower model ("sonnet으로 진행", "그대로 진행", "force", or similar): proceed to Step 1, but prepend a single-line warning at the top of the generated schedule file: `> ⚠️ Generated with <model> (not Opus). Cross-task synthesis quality may be reduced.`
67
- 5. Skip this gate ONLY when the harness has clearly enforced `model: opus` from the frontmatter — verifiable by the active model already being Opus-class or above without manual switching.
67
+
68
+ This is a recommendation gate, not a blocking mechanism — the override path in step 4 always lets the run proceed.
68
69
 
69
70
  ### Step 1: Resolve task-group and collect tasks
70
71
 
@@ -76,7 +77,7 @@ This skill performs cross-task synthesis (multi-task classification, dependency
76
77
 
77
78
  ### Step 2: Filter by workStatus
78
79
 
79
- 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.
80
+ `workStatus` is also surfaced directly inside each catalog entry, but Step 1 still re-reads each `task-manifest.json` because the manifest is authoritative no extra fetch is needed beyond that.
80
81
 
81
82
  For inference rules when `workStatus` is missing or empty, **defer to the inference table in `skills/okstra-inspect/SKILL.md` (`status.4` → "Default value convention")** instead of duplicating it here. Apply that table to derive a working value, then filter:
82
83
 
@@ -131,7 +132,7 @@ Claude lead synthesises the schedule directly using collected data. Proceed to S
131
132
 
132
133
  Classify each task into one of three phases based on report data.
133
134
 
134
- **`workCategory` accepted values** (de-facto enum, from `scripts/okstra_ctl/worktree.py::_WORK_CATEGORY_PREFIX` plus the `unknown` fallback emitted by `render.py`):
135
+ **`workCategory` accepted values** the category keys come from `scripts/okstra_ctl/worktree.py::_WORK_CATEGORY_PREFIX` (code source), plus the `unknown` fallback emitted by `render.py`. The phase mapping below is layered on top: when a category is added to `worktree.py`, add a row here too — otherwise the new category falls through to `unknown` → Phase 2.
135
136
 
136
137
  | workCategory | Default phase |
137
138
  |--------------|---------------|
@@ -329,7 +330,7 @@ If a directive source is found and contains a `## Directive` section that affect
329
330
 
330
331
  A directive file that exists but contains no `## Directive` heading is treated as "no directive" — fall through to the heuristic, no warning required.
331
332
 
332
- `## Gantt Chart` is the only `##` section that MAY be added beyond the 11 mandatory ones. Any other extra `##` heading is forbidden.
333
+ `## Gantt Chart` is the only `##` section that MAY be added beyond the 10 mandatory ones. Any other extra `##` heading is forbidden.
333
334
 
334
335
  #### MUST — emit a skip-reason note when omitting Gantt Chart (rare case)
335
336
 
@@ -416,7 +417,7 @@ Required shapes:
416
417
  | Pattern | Why forbidden |
417
418
  |---------|---------------|
418
419
  | Translating any `##` heading to Korean (`전체 개요`, `요약`, `위험 완화 전략`, `즉시 권장 액션`, etc.) or any other language | Heading literals are stable identifiers used by the validator and downstream tooling |
419
- | Adding sections not in the contract (`## Effort-to-Day 매핑`, `## Référentiel …`, etc. — anything beyond the 11 mandatory + the 1 optional `## Gantt Chart`) | Sizing tables belong inside `### Effort Sizing 기준` (Executive Summary). All other graphic detail belongs inside per-phase prose |
420
+ | Adding sections not in the contract (`## Effort-to-Day 매핑`, `## Référentiel …`, etc. — anything beyond the 10 mandatory + the 1 optional `## Gantt Chart`) | Sizing tables belong inside `### Effort Sizing 기준` (Executive Summary). All other graphic detail belongs inside per-phase prose |
420
421
  | Translating the optional graphic section (`## Gantt 다이어그램`, `## Diagramme de Gantt`) | Use `## Gantt Chart` literally — translation is forbidden |
421
422
  | Adding `## Cumulative Timeline` (or any translated form: `## 누적 일정표`, `## Calendrier Cumulé`) | The Cumulative Timeline section has been removed from the contract — never emit it |
422
423
  | Using calendar dates / weekdays / "today + N" labels in the Gantt axis (e.g. `2026-05-04`, `Mon May 4`, `오늘 + 3일`) | The axis MUST be relative day-counts (`Day 1`, `J1`, …) — the schedule describes effort, not a wall-clock plan |
@@ -444,7 +445,7 @@ After writing the file and before printing the completion message in Step 7, you
444
445
  python3 ~/.okstra/lib/validators/validate-schedule.py <output-path>
445
446
  ```
446
447
  3. If the validator exits non-zero, **fix the file and re-validate**. Do not print the completion message until the validator passes.
447
- 4. If `~/.okstra/lib/validators/validate-schedule.py` is not present, fall back to a manual checklist: confirm each of the 11 mandatory headings from `### MUST — exact ordered heading list` (above) appears in the file with that exact spelling, the title ends with `— Work Schedule`, and the metadata `> Generated:` block is present.
448
+ 4. If `~/.okstra/lib/validators/validate-schedule.py` is not present, fall back to a manual checklist: confirm each of the 10 mandatory headings from `### MUST — exact ordered heading list` (above) appears in the file with that exact spelling, the title ends with `— Work Schedule`, and the metadata `> Generated:` block is present.
448
449
 
449
450
  ## Schedule Document Template
450
451
 
@@ -454,15 +455,7 @@ The two rules below complement the template — they encode COMPUTATION that the
454
455
 
455
456
  ### Effort-to-Day mapping (At a Glance total)
456
457
 
457
- | Size | Day(s) |
458
- |------|----------|
459
- | S | 0.5 - 1 |
460
- | M | 2 - 3 |
461
- | L | 3 - 5 |
462
- | XL | 5 - 10 |
463
- | XXL | 10 - |
464
-
465
- Sum the lower bounds across all in-scope tasks for the lower total, and the upper bounds for the upper total. The same table is rendered as descriptive reference under `### Effort Sizing 기준`; this copy is the source-of-truth for the At a Glance computation.
458
+ Day-range values per size are defined once in the template's `### Effort Sizing 기준` table (`~/.okstra/templates/reports/schedule.template.md`) — that table is the SSOT. To compute the At a Glance total, read its `Day(s)` column and sum the lower bounds across all in-scope tasks for the lower total, the upper bounds for the upper total.
466
459
 
467
460
  ### `[NEEDS-OKSTRA-RUN]` / `[PARSE-ERROR: <section>]` placement
468
461
 
@@ -74,6 +74,9 @@ Parse the JSON from stdout. The shape is `{ok, projectRoot, projectJsonPath, pro
74
74
  okstra check-project --cwd /abs/path/from/user --json
75
75
  ```
76
76
 
77
+ - `ok: false`, `stage: "project_json_missing"` → proceed to Step 4 (this is the normal create path).
78
+ - `ok: false`, any other `stage` (`python`, `parse`, `project_json_invalid`) → show the JSON `reason` field to the user verbatim and follow the recovery it names (typically `okstra doctor` to diagnose, then `okstra ensure-installed` or re-running the Step 1 install). `reason` is the SSOT — do not hand-enumerate stage causes.
79
+
77
80
  ## Step 4: Inspect or create `project.json`
78
81
 
79
82
  Use the `Read` tool on the literal absolute path `<projectRoot>/.okstra/project.json` (substitute the literal `projectRoot` value parsed in Step 3). If `Read` errors with "file does not exist", treat that as the "create" branch below; otherwise the file exists and you can inspect its contents inline.
@@ -163,14 +166,12 @@ commands whose language is not represented in this run's diff.
163
166
 
164
167
  **`cmd` deny-list (mutation guard):** the verifier rejects any `cmd`
165
168
  containing tokens that imply mutation — declare commands in their
166
- check-only form only. Denied tokens include:
167
-
168
- - `--fix` (eslint, ruff), `--write` (prettier), ` -w` (gofmt),
169
- - ` -u` / `--updateSnapshot` / `--snapshot-update` / `--update-goldens`,
170
- - `INSTA_UPDATE=` with any value other than `no`,
171
- - `cargo insta accept`,
172
- - `npm install` (use `npm ci`), `cargo update`, `pip install -U`,
173
- - `pnpm add`, `bun add`, `cargo add`.
169
+ check-only form only. Representative denied tokens: `--fix` / `--write`
170
+ (formatters), `--updateSnapshot`, `npm install` (use `npm ci`),
171
+ `pip install -U`, `cargo add`. The authoritative deny list is
172
+ `scripts/okstra_ctl/qa_commands.py::find_denied_tokens`
173
+ (`_DENIED_LITERAL_TOKENS` / `_DENIED_SUBSTRINGS` plus the dynamic
174
+ `npm install` and `INSTA_UPDATE=` checks) — do not re-enumerate it here.
174
175
 
175
176
  Encountering a denied token aborts the verifier with status
176
177
  `contract-violated`; re-declare the command in check-only form to
@@ -338,6 +339,6 @@ Inform the user with a short summary:
338
339
  | `okstra ensure-installed` keeps reinstalling | `~/.okstra/version` write fails (permissions) | Check `~/.okstra` ownership and writability. |
339
340
  | `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`. |
340
341
  | `projectId mismatch` / `projectId 불일치` | `project.json` already exists with a different id | Decide which id is canonical; manually delete `<PROJECT_ROOT>/.okstra/project.json` to re-register, or re-run with the existing id. |
341
- | `EACCES` writing under `.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. |
342
+ | `EACCES` writing under `.okstra/` | directory owned by another user (e.g. created by a previous root-shell run) | `chown -R "$USER" <PROJECT_ROOT>/.okstra` or delete and let setup recreate. |
342
343
  | `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. |
343
344
  | `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>`. |
@@ -23,7 +23,7 @@ candidate-cap: 8 # codebase-scan only: 1..12, de
23
23
  > Source type: <file | linear | jira | github | notion | url | user-input>
24
24
  > Tracker key (if any): <LIN-1234 | PROJ-42 | gh-repo-123 | notion-abcdef12>
25
25
  > Parent brief (child briefs only): <relative path>
26
- > Recommended next phase: <requirements-discovery | error-analysis> ← from Step 6
26
+ > Recommended next phase: <requirements-discovery | error-analysis | improvement-discovery> ← from Step 6
27
27
  > Handoff contract: see `prompts/profiles/_common-contract.md` § "Brief handoff contract"
28
28
 
29
29
  ## Source Material
@@ -573,7 +573,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
573
573
  {% if header.taskType == 'improvement-discovery' %}
574
574
  ## 5.9 Improvement Candidates
575
575
 
576
- > Lens whitelist and column schema enforced by `validators/validate-improvement-report.py`. Row count is bounded by the brief's `candidate-cap` (default 8, absolute max 12).
576
+ > Lens whitelist and column schema enforced by `validators/validate_improvement_report.py`. Row count is bounded by the brief's `candidate-cap` (default 8, absolute max 12).
577
577
 
578
578
  | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
579
579
  |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|