okstra 0.34.0 → 0.36.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 (101) hide show
  1. package/README.kr.md +26 -16
  2. package/README.md +26 -16
  3. package/docs/kr/architecture.md +59 -45
  4. package/docs/kr/cli.md +61 -18
  5. package/docs/pr-template-usage.md +65 -0
  6. package/docs/project-structure-overview.md +358 -354
  7. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
  8. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
  9. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
  10. package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
  11. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
  12. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
  13. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
  14. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
  15. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
  16. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
  17. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
  18. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
  19. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
  20. package/docs/task-process/README.md +74 -0
  21. package/docs/task-process/common-flow.md +166 -0
  22. package/docs/task-process/error-analysis.md +101 -0
  23. package/docs/task-process/final-verification.md +167 -0
  24. package/docs/task-process/implementation-planning.md +128 -0
  25. package/docs/task-process/implementation.md +149 -0
  26. package/docs/task-process/release-handoff.md +206 -0
  27. package/docs/task-process/requirements-discovery.md +115 -0
  28. package/package.json +1 -1
  29. package/runtime/BUILD.json +2 -2
  30. package/runtime/agents/SKILL.md +29 -13
  31. package/runtime/agents/workers/claude-worker.md +26 -0
  32. package/runtime/agents/workers/codex-worker.md +27 -1
  33. package/runtime/agents/workers/gemini-worker.md +27 -1
  34. package/runtime/agents/workers/report-writer-worker.md +8 -1
  35. package/runtime/bin/okstra-central.sh +6 -6
  36. package/runtime/bin/okstra-codex-exec.sh +49 -28
  37. package/runtime/bin/okstra-gemini-exec.sh +39 -21
  38. package/runtime/bin/okstra-render-final-report.py +13 -2
  39. package/runtime/bin/okstra-wrapper-status.py +155 -0
  40. package/runtime/bin/okstra.sh +2 -2
  41. package/runtime/prompts/profiles/_common-contract.md +11 -6
  42. package/runtime/prompts/profiles/error-analysis.md +3 -7
  43. package/runtime/prompts/profiles/implementation-planning.md +22 -21
  44. package/runtime/prompts/profiles/implementation.md +28 -11
  45. package/runtime/prompts/profiles/improvement-discovery.md +42 -0
  46. package/runtime/prompts/profiles/kr/_common-contract.md +92 -0
  47. package/runtime/prompts/profiles/kr/error-analysis.md +36 -0
  48. package/runtime/prompts/profiles/kr/final-verification.md +48 -0
  49. package/runtime/prompts/profiles/kr/implementation-planning.md +90 -0
  50. package/runtime/prompts/profiles/kr/implementation.md +144 -0
  51. package/runtime/prompts/profiles/kr/improvement-discovery.md +42 -0
  52. package/runtime/prompts/profiles/kr/release-handoff.md +104 -0
  53. package/runtime/prompts/profiles/kr/requirements-discovery.md +42 -0
  54. package/runtime/prompts/profiles/release-handoff.md +1 -1
  55. package/runtime/prompts/profiles/requirements-discovery.md +8 -12
  56. package/runtime/prompts/wizard/prompts.ko.json +230 -0
  57. package/runtime/python/lib/okstra/cli.sh +2 -49
  58. package/runtime/python/lib/okstra/globals.sh +21 -21
  59. package/runtime/python/lib/okstra/interactive.sh +7 -7
  60. package/runtime/python/okstra_ctl/clarification_items.py +3 -9
  61. package/runtime/python/okstra_ctl/consumers.py +53 -0
  62. package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
  63. package/runtime/python/okstra_ctl/i18n.py +73 -0
  64. package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
  65. package/runtime/python/okstra_ctl/index.py +1 -1
  66. package/runtime/python/okstra_ctl/paths.py +23 -20
  67. package/runtime/python/okstra_ctl/render.py +147 -202
  68. package/runtime/python/okstra_ctl/render_final_report.py +53 -10
  69. package/runtime/python/okstra_ctl/run.py +292 -107
  70. package/runtime/python/okstra_ctl/run_context.py +22 -0
  71. package/runtime/python/okstra_ctl/seeding.py +186 -0
  72. package/runtime/python/okstra_ctl/wizard.py +348 -127
  73. package/runtime/python/okstra_ctl/workflow.py +21 -2
  74. package/runtime/python/okstra_ctl/worktree.py +54 -1
  75. package/runtime/python/okstra_project/resolver.py +4 -3
  76. package/runtime/python/okstra_token_usage/report.py +2 -2
  77. package/runtime/schemas/final-report-v1.0.schema.json +22 -16
  78. package/runtime/skills/okstra-brief/SKILL.md +124 -31
  79. package/runtime/skills/okstra-convergence/SKILL.md +2 -3
  80. package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
  81. package/runtime/skills/okstra-run/SKILL.md +5 -4
  82. package/runtime/skills/okstra-schedule/SKILL.md +4 -4
  83. package/runtime/skills/okstra-setup/SKILL.md +27 -0
  84. package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
  85. package/runtime/templates/okstra.CLAUDE.md +104 -0
  86. package/runtime/templates/reports/final-report.template.md +93 -98
  87. package/runtime/templates/reports/i18n/en.json +135 -0
  88. package/runtime/templates/reports/i18n/ko.json +135 -0
  89. package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
  90. package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
  91. package/runtime/templates/reports/task-brief.template.md +2 -2
  92. package/runtime/validators/lib/fixtures.sh +30 -0
  93. package/runtime/validators/lib/runners.sh +1 -1
  94. package/runtime/validators/validate-implementation-plan-stages.py +211 -0
  95. package/runtime/validators/validate-run.py +121 -26
  96. package/runtime/validators/validate-workflow.sh +2 -2
  97. package/runtime/validators/validate_improvement_report.py +275 -0
  98. package/src/config.mjs +18 -0
  99. package/src/install.mjs +41 -14
  100. package/src/setup.mjs +133 -1
  101. package/src/uninstall.mjs +21 -1
@@ -15,10 +15,13 @@ state passing, and are read once at the start.
15
15
  """
16
16
  from __future__ import annotations
17
17
 
18
+ import importlib.util
18
19
  import json
19
20
  import os
20
21
  import re
21
22
  import shutil
23
+ import subprocess as _subprocess
24
+ import sys
22
25
  from dataclasses import dataclass, field
23
26
  from datetime import datetime, timezone
24
27
  from pathlib import Path
@@ -35,6 +38,8 @@ from .material import (
35
38
  from .models import resolve_model_metadata
36
39
  from .path_resolve import relative_to_project_root, resolve_user_file
37
40
  from .render import (
41
+ apply_lead_prompt_defaults,
42
+ inject_lead_prompt_computed_tokens,
38
43
  render_latest_task_discovery,
39
44
  render_reference_expectations,
40
45
  render_run_manifest,
@@ -42,13 +47,17 @@ from .render import (
42
47
  render_task_index,
43
48
  render_task_manifest,
44
49
  render_team_state,
45
- render_template_file,
50
+ render_template_with_ctx,
46
51
  render_timeline,
47
52
  )
48
53
  from .run_context import compute_and_write_run_context, write_run_inputs
49
54
  from .seeding import (
55
+ AgentsMdLinkError,
56
+ ClaudeMdLinkError,
50
57
  SettingsLinkError,
51
58
  cleanup_obsolete_generated_docs,
59
+ ensure_project_agents_md,
60
+ ensure_project_claude_md,
52
61
  ensure_project_settings_symlink,
53
62
  installed_version,
54
63
  verify_installation,
@@ -67,21 +76,30 @@ from .workers import (
67
76
  from .workflow import compute_workflow_state
68
77
  from .worktree import provision_task_worktree
69
78
 
70
- # Validator regex for the approval marker.
79
+ # Frontmatter approval-flag matcher.
71
80
  #
72
- # Tolerates a single optional backtick on either side of the approval token,
73
- # because the report template instructs the user to flip `[ ]` to `[x]` inside
74
- # a markdown code span and the report-writer worker often emits a standalone
75
- # marker line wrapped the same way (e.g. `- ` + backtick + `[x] Approved` +
76
- # backtick). Backticks carry no semantic content here — stripping them at the
77
- # parser level is simpler than threading a "please remove formatting" rule
78
- # through every authoring surface.
79
- APPROVED_PLAN_PATTERN = re.compile(
80
- r"^[ \t]*(?:[-*+][ \t]+)?`?(APPROVED([ \t]|:|$|`)|\[x\][ \t]*Approved`?|"
81
- r"User[ \t]+Approval[ \t]*:[ \t]*(APPROVED|granted|yes)`?)",
81
+ # Final-report YAML frontmatter 안에서 `approved: true` / `approved: false`
82
+ # 줄만 식별한다. report-writer 항상 줄을 출력하고, 사용자가
83
+ # 직접 편집하거나 `--approve` CLI 줄만 toggle 한다. 본문(body) 다른
84
+ # `approved:` 등장과 충돌하지 않도록 호출자는 frontmatter 블록을 먼저 추출
85
+ # (`_extract_frontmatter_block`) 패턴을 적용한다.
86
+ APPROVED_FRONTMATTER_PATTERN = re.compile(
87
+ r"^approved:[ \t]+(true|false)[ \t]*$",
82
88
  re.IGNORECASE | re.MULTILINE,
83
89
  )
84
90
 
91
+ _FRONTMATTER_BLOCK_PATTERN = re.compile(r"\A---\n(.*?)\n---\n", re.DOTALL)
92
+
93
+
94
+ def _extract_frontmatter_block(body: str) -> str | None:
95
+ """final-report 의 leading `---` 펜스 사이에 든 YAML 블록 텍스트를 반환.
96
+
97
+ 펜스가 없거나 닫히지 않으면 None. report-writer 의 표준 산출물은 항상
98
+ `---\\n...frontmatter...\\n---\\n` 로 시작한다.
99
+ """
100
+ m = _FRONTMATTER_BLOCK_PATTERN.match(body)
101
+ return m.group(1) if m else None
102
+
85
103
 
86
104
  class PrepareError(Exception):
87
105
  """surface to caller — task bundle prepare failed."""
@@ -108,6 +126,7 @@ class PrepareInputs:
108
126
  work_category: str = ""
109
127
  base_ref: str = ""
110
128
  approved_plan_path: str = ""
129
+ stage: str = "auto"
111
130
  clarification_response_path: str = "" # absolute or empty
112
131
  # release-handoff 전용: PR 본문 템플릿 1회성 override. 빈 문자열이면
113
132
  # project.json → global config → 스킬 디폴트 순으로 해석된다.
@@ -137,25 +156,35 @@ def _validate_approved_plan(path: str) -> None:
137
156
  if not p.is_file():
138
157
  raise PrepareError(f"approved plan file not found: {path}")
139
158
  body = p.read_text(encoding="utf-8", errors="replace")
140
- if not APPROVED_PLAN_PATTERN.search(body):
159
+ frontmatter = _extract_frontmatter_block(body)
160
+ if frontmatter is None:
141
161
  raise PrepareError(
142
- f"approved plan has no recognised user-approval marker: {path}\n"
143
- ' canonical form (single line, top-of-report block): "- [x] Approved"\n'
144
- ' also accepted (case-insensitive, line-anchored, optional leading bullet): '
145
- '"APPROVED", "[x] Approved", "User Approval: APPROVED|granted|yes"\n'
146
- " shortcut: re-run okstra with --approve to have the CLI itself "
147
- "record the approval marker on this file."
162
+ f"approved plan has no YAML frontmatter block: {path}\n"
163
+ " expected the report to begin with `---\\n...\\n---\\n`. "
164
+ "report-writer worker emits this header on every run; "
165
+ "regenerate the report if the header is missing."
148
166
  )
149
- # The approval marker is set. Cross-check the §5 Clarification Items
150
- # table: any row with Blocks=approval that is still open/answered
151
- # invalidates the approval. Legacy reports (no unified §5 with a
152
- # Blocks column) return None soft-pass to preserve compatibility
153
- # during the transitional release.
167
+ m = APPROVED_FRONTMATTER_PATTERN.search(frontmatter)
168
+ if not m:
169
+ raise PrepareError(
170
+ f"approved plan frontmatter has no `approved:` field: {path}\n"
171
+ " expected a single line of the form `approved: true` "
172
+ "(or `approved: false` for the unflipped state)."
173
+ )
174
+ if m.group(1).lower() != "true":
175
+ raise PrepareError(
176
+ f"approved plan is not yet approved (frontmatter `approved: {m.group(1)}`): {path}\n"
177
+ " open the report and change the frontmatter line to `approved: true`, "
178
+ "or re-run okstra with `--approve` to flip it from the CLI.\n"
179
+ " resolve any `Blocks=approval` rows in `## 5. Clarification Items` first."
180
+ )
181
+ # frontmatter approved == true 상태. §5 Clarification Items 의
182
+ # Blocks=approval 행이 아직 open/answered 면 승인을 무효화한다.
154
183
  blockers = unresolved_approval_blockers(body)
155
184
  if blockers:
156
185
  lines = [
157
- f"approved plan marker is set but §5 has {len(blockers)} unresolved "
158
- f"`Blocks=approval` row(s); resolve them or mark them obsolete before approving:",
186
+ f"approved plan frontmatter has `approved: true` but §5 has {len(blockers)} "
187
+ f"unresolved `Blocks=approval` row(s); resolve them or mark them obsolete first:",
159
188
  ]
160
189
  for b in blockers:
161
190
  lines.append(f" - {b.row_id} (Status={b.raw_status})")
@@ -163,31 +192,121 @@ def _validate_approved_plan(path: str) -> None:
163
192
  raise PrepareError("\n".join(lines))
164
193
 
165
194
 
166
- # `- [ ] Approved` 라인을 정확히 한 번만 매치한다. 좌측 leading whitespace 와
167
- # 옵션 bullet 은 보존된 채 체크박스 안쪽 공백만 `x` 로 갱신된다.
168
- #
169
- # Group 1: leading whitespace + optional bullet + optional opening backtick.
170
- # Group 2: optional closing backtick + trailing whitespace.
171
- # Both groups are preserved verbatim in the replacement so a backtick-wrapped
172
- # `- \`[ ] Approved\`` flips to `- \`[x] Approved\`` without losing the
173
- # surrounding code span — the validator regex tolerates either form.
174
- APPROVAL_UNCHECKED_PATTERN = re.compile(
175
- r"^([ \t]*(?:[-*+][ \t]+)?`?)\[[ \t]\][ \t]*Approved(`?[ \t]*)$",
176
- re.IGNORECASE | re.MULTILINE,
195
+ _STAGE_VALIDATOR_PATH = (
196
+ Path(__file__).resolve().parents[2] / "validators"
197
+ / "validate-implementation-plan-stages.py"
177
198
  )
178
199
 
179
200
 
201
+ def _validate_stage_structure(plan_path: str) -> None:
202
+ """Run validators/validate-implementation-plan-stages.py against the approved plan."""
203
+ r = _subprocess.run(
204
+ [sys.executable, str(_STAGE_VALIDATOR_PATH), "--plan", plan_path],
205
+ capture_output=True, text=True,
206
+ )
207
+ if r.returncode != 0:
208
+ raise PrepareError(
209
+ f"approved plan failed stage validation:\n{r.stderr.strip()}"
210
+ )
211
+
212
+
213
+ def _resolve_effective_stage(
214
+ stages: list,
215
+ done_stages: set,
216
+ requested: str,
217
+ ) -> int:
218
+ """Return the stage number to execute.
219
+
220
+ `requested` is either "auto" or a decimal string.
221
+ Raises PrepareError on all rejection cases.
222
+ """
223
+ if requested == "auto":
224
+ for s in stages:
225
+ if s["stage_number"] in done_stages:
226
+ continue
227
+ if all(d in done_stages for d in s["depends_on"]):
228
+ return s["stage_number"]
229
+ raise PrepareError(
230
+ "no stage is ready: every remaining stage has unsatisfied depends-on"
231
+ )
232
+ try:
233
+ n = int(requested)
234
+ except ValueError:
235
+ raise PrepareError(
236
+ f"--stage must be 'auto' or an integer, got {requested!r}"
237
+ )
238
+ target = next((s for s in stages if s["stage_number"] == n), None)
239
+ if target is None:
240
+ raise PrepareError(
241
+ f"--stage {n} not in Stage Map "
242
+ f"(have {[s['stage_number'] for s in stages]})"
243
+ )
244
+ if n in done_stages:
245
+ raise PrepareError(
246
+ f"--stage {n} already completed (consumers.jsonl status:done exists)"
247
+ )
248
+ return n
249
+
250
+
251
+ def _parse_stage_map_into_ctx(plan_path: str) -> list:
252
+ """Reuse the validator's parser to extract StageMeta dicts for the ctx."""
253
+ text = Path(plan_path).read_text(encoding="utf-8")
254
+ spec = importlib.util.spec_from_file_location(
255
+ "_ip_stage_validator", _STAGE_VALIDATOR_PATH
256
+ )
257
+ if spec is None or spec.loader is None:
258
+ raise PrepareError(f"cannot load stage validator at {_STAGE_VALIDATOR_PATH}")
259
+ mod = importlib.util.module_from_spec(spec)
260
+ # Register before exec_module so dataclass field-type resolution can find
261
+ # the module in sys.modules (required on Python 3.9).
262
+ sys.modules["_ip_stage_validator"] = mod
263
+ try:
264
+ spec.loader.exec_module(mod)
265
+ finally:
266
+ sys.modules.pop("_ip_stage_validator", None)
267
+ stages, _errs = mod._parse_stage_map(text)
268
+ return [
269
+ {
270
+ "stage_number": s.stage_number,
271
+ "title": s.title,
272
+ "depends_on": list(s.depends_on),
273
+ "step_count": s.step_count,
274
+ "exit_contract_summary": s.exit_contract_summary,
275
+ }
276
+ for s in stages
277
+ ]
278
+
279
+
180
280
  def _apply_cli_approval(path: str) -> str:
181
- """`--approve` 가 지정된 경우 approved-plan 파일에 사용자 승인 마커를 새겨 넣는다.
281
+ """`--approve` 가 지정된 경우 approved-plan frontmatter `approved` true 로 토글.
282
+
283
+ 동작 요약:
284
+ - frontmatter 에 `approved: false` 가 있으면 `approved: true` 로 교체하고
285
+ audit 라인을 append → `"frontmatter-flipped"` 반환.
286
+ - 이미 `approved: true` 면 한 번도 기록되지 않은 audit 라인만 append →
287
+ `"already-approved-audit-appended"`, 이미 동일 audit 가 있으면
288
+ `"already-approved"` 로 no-op.
289
+ - frontmatter 가 없거나 `approved:` 라인이 없으면 PrepareError.
182
290
 
183
- Returns a short human-readable description of the action taken (used in the
184
- runtime audit line). Idempotent: if the file already carries a valid
185
- approval marker, no edits are written and `"already-approved"` is returned.
291
+ Idempotent: 동일 audit 라인은 번만 기록된다.
186
292
  """
187
293
  p = Path(path)
188
294
  if not p.is_file():
189
295
  raise PrepareError(f"approved plan file not found: {path}")
190
296
  body = p.read_text(encoding="utf-8", errors="replace")
297
+ frontmatter = _extract_frontmatter_block(body)
298
+ if frontmatter is None:
299
+ raise PrepareError(
300
+ f"--approve was given but the approved-plan file has no YAML frontmatter: {path}\n"
301
+ " expected the report to begin with `---\\n...\\n---\\n`."
302
+ )
303
+ m = APPROVED_FRONTMATTER_PATTERN.search(frontmatter)
304
+ if not m:
305
+ raise PrepareError(
306
+ f"--approve was given but the approved-plan frontmatter has no `approved:` field: {path}\n"
307
+ " expected a single line of the form `approved: false` "
308
+ "(report-writer worker emits this by default)."
309
+ )
191
310
 
192
311
  audit_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
193
312
  audit_line = (
@@ -195,37 +314,28 @@ def _apply_cli_approval(path: str) -> str:
195
314
  "(user CLI invocation treated as approval signal)"
196
315
  )
197
316
 
198
- if APPROVED_PLAN_PATTERN.search(body):
199
- # 이미 사용자(또는 이전 --approve 호출)가 마커를 남긴 상태. audit 라인이
200
- # 없으면 보조적으로 한 줄만 추가하고 마커 자체는 건드리지 않는다.
317
+ if m.group(1).lower() == "true":
201
318
  if audit_line.split(" — ")[1] in body:
202
319
  return "already-approved"
203
320
  new_body = body.rstrip("\n") + "\n" + audit_line + "\n"
204
321
  p.write_text(new_body, encoding="utf-8")
205
322
  return "already-approved-audit-appended"
206
323
 
207
- if APPROVAL_UNCHECKED_PATTERN.search(body):
208
- new_body, count = APPROVAL_UNCHECKED_PATTERN.subn(
209
- lambda m: f"{m.group(1)}[x] Approved{m.group(2)}", body, count=1,
210
- )
211
- new_body = new_body.rstrip("\n") + "\n" + audit_line + "\n"
212
- p.write_text(new_body, encoding="utf-8")
213
- return "checkbox-flipped"
214
-
215
- raise PrepareError(
216
- f"--approve was given but the approved-plan file has no `User Approval Request` "
217
- f"checkbox to flip: {path}\n"
218
- " expected a line of the form `- [ ] Approved` near the top of the report "
219
- "(see templates/reports/final-report.template.md)."
324
+ flipped_frontmatter = APPROVED_FRONTMATTER_PATTERN.sub(
325
+ "approved: true", frontmatter, count=1,
220
326
  )
327
+ new_body = body.replace(frontmatter, flipped_frontmatter, 1)
328
+ new_body = new_body.rstrip("\n") + "\n" + audit_line + "\n"
329
+ p.write_text(new_body, encoding="utf-8")
330
+ return "frontmatter-flipped"
221
331
 
222
332
 
223
333
  def _ensure_task_directories(ctx: dict) -> None:
224
334
  for key in (
225
- "TASK_ROOT", "INSTRUCTION_SET_DIR", "RUNS_DIR", "HISTORY_DIR",
335
+ "TASK_ROOT", "INSTRUCTION_SET_PATH", "RUNS_DIR", "HISTORY_DIR",
226
336
  "RUN_DIR", "RUN_MANIFESTS_DIR", "RUN_STATE_DIR", "RUN_PROMPTS_DIR",
227
337
  "RUN_REPORTS_DIR", "RUN_STATUS_DIR", "RUN_SESSIONS_DIR",
228
- "RUN_LOGS_DIR", "WORKER_RESULTS_DIR", "OKSTRA_DISCOVERY_DIR",
338
+ "RUN_LOGS_DIR", "WORKER_RESULTS_PATH", "RUN_CARRY_PATH", "OKSTRA_DISCOVERY_DIR",
229
339
  ):
230
340
  Path(ctx[key]).mkdir(parents=True, exist_ok=True)
231
341
 
@@ -329,11 +439,11 @@ def _record_start(
329
439
  project_root=ctx["PROJECT_ROOT"],
330
440
  task_group=ctx["TASK_GROUP"],
331
441
  task_id=ctx["TASK_ID"],
332
- task_type=ctx.get("ANALYSIS_TYPE", ""),
442
+ task_type=ctx.get("TASK_TYPE", ""),
333
443
  run_seq=int(ctx["RUN_MANIFESTS_SEQ"]),
334
444
  when=ctx["RUN_TIMESTAMP_ISO"],
335
- workers=[w for w in ctx.get("SELECTED_REVIEWERS", "").split(",") if w],
336
- lead_model=ctx.get("LEAD_MODEL_DISPLAY", ""),
445
+ workers=[w for w in ctx.get("RECOMMENDED_ANALYSERS", "").split(",") if w],
446
+ lead_model=ctx.get("LEAD_MODEL", ""),
337
447
  run_dir_rel=ctx.get("RUN_DIR_RELATIVE_PATH", ""),
338
448
  final_report_rel=ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
339
449
  final_status_rel=ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
@@ -358,7 +468,7 @@ def _brief_sha256(path: Path) -> str:
358
468
 
359
469
  def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
360
470
  """rerun 충실 재현을 위한 canonical argv 재구성."""
361
- workers = inp.workers_override or ctx.get("SELECTED_REVIEWERS", "")
471
+ workers = inp.workers_override or ctx.get("RECOMMENDED_ANALYSERS", "")
362
472
  pairs = [
363
473
  ("--task-type", inp.task_type),
364
474
  ("--project-id", inp.project_id),
@@ -369,11 +479,11 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
369
479
  ("--approved-plan", inp.approved_plan_path),
370
480
  ("--clarification-response", inp.clarification_response_path),
371
481
  ("--workers", workers),
372
- ("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL_DISPLAY", "")),
373
- ("--claude-model", inp.claude_model or ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", "")),
374
- ("--codex-model", inp.codex_model or ctx.get("CODEX_WORKER_MODEL_DISPLAY", "")),
375
- ("--gemini-model", inp.gemini_model or ctx.get("GEMINI_WORKER_MODEL_DISPLAY", "")),
376
- ("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL_DISPLAY", "")),
482
+ ("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL", "")),
483
+ ("--claude-model", inp.claude_model or ctx.get("CLAUDE_WORKER_MODEL", "")),
484
+ ("--codex-model", inp.codex_model or ctx.get("CODEX_WORKER_MODEL", "")),
485
+ ("--gemini-model", inp.gemini_model or ctx.get("GEMINI_WORKER_MODEL", "")),
486
+ ("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL", "")),
377
487
  ("--executor", inp.executor or ctx.get("EXECUTOR_PROVIDER", "")),
378
488
  ("--related-tasks", inp.related_tasks_raw),
379
489
  ("--work-category", inp.work_category),
@@ -471,17 +581,25 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
471
581
  )
472
582
  if inp.approve_plan_ack:
473
583
  # 사용자가 직접 `--approve` 를 입력한 행위 자체를 승인 의사로 모델링한다.
474
- # 파일을 먼저 갱신한 동일한 검증 경로를 그대로 통과시킨다 검증
475
- # 책임을 단일 지점(`_validate_approved_plan`)으로 유지한다.
584
+ # 파일의 frontmatter approved true toggle 동일한 검증
585
+ # 경로(`_validate_approved_plan`) 를 그대로 통과시킨다.
476
586
  _apply_cli_approval(inp.approved_plan_path)
477
587
  _validate_approved_plan(inp.approved_plan_path)
478
- elif inp.approve_plan_ack:
479
- # implementation 외 task-type 에서 `--approve` 는 의미가 없다. 사용자에게
480
- # 정확한 시점을 알려주기 위해 조용히 무시하지 않고 즉시 거부한다.
481
- raise PrepareError(
482
- "--approve is only meaningful with --task-type implementation "
483
- "and --approved-plan <path>"
484
- )
588
+ _validate_stage_structure(inp.approved_plan_path)
589
+ ctx_stage_map = _parse_stage_map_into_ctx(inp.approved_plan_path)
590
+ else:
591
+ if inp.approve_plan_ack:
592
+ # implementation task-type 에서 `--approve` 는 의미가 없다. 사용자에게
593
+ # 정확한 시점을 알려주기 위해 조용히 무시하지 않고 즉시 거부한다.
594
+ raise PrepareError(
595
+ "--approve is only meaningful with --task-type implementation "
596
+ "and --approved-plan <path>"
597
+ )
598
+ if inp.stage != "auto":
599
+ raise PrepareError(
600
+ f"--stage is only meaningful with --task-type implementation; "
601
+ f"got {inp.task_type}"
602
+ )
485
603
  if inp.clarification_response_path and not Path(inp.clarification_response_path).is_file():
486
604
  raise PrepareError(
487
605
  f"clarification response file not found: {inp.clarification_response_path}"
@@ -660,7 +778,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
660
778
  profile_content = _expand_profile_includes(profile_file)
661
779
  review_material = build_analysis_material(inp.brief_path, inp.directive)
662
780
  related_items = resolve_related_tasks(
663
- task_manifest_path=Path(ctx["TASK_MANIFEST_FILE"]),
781
+ task_manifest_path=Path(ctx["TASK_MANIFEST_PATH"]),
664
782
  raw_related=inp.related_tasks_raw,
665
783
  )
666
784
  related_tasks_json_str = json.dumps(related_items, ensure_ascii=False)
@@ -686,25 +804,24 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
686
804
 
687
805
  # ---- assemble full ctx (the values render functions expect) ----
688
806
  ctx.update({
689
- "REVIEW_PROFILE": inp.task_type,
690
- "SELECTED_REVIEWERS": selected_reviewers,
807
+ "ANALYSIS_PROFILE": inp.task_type,
808
+ "RECOMMENDED_ANALYSERS": selected_reviewers,
691
809
  "PR_TEMPLATE_PATH": pr_template_path_str,
692
810
  "PR_TEMPLATE_SOURCE": pr_template_source,
693
811
  "CLAUDE_SESSION_ID": claude_session_id,
694
812
  "CLARIFICATION_RESPONSE_PATH": inp.clarification_response_path,
695
- "CLARIFICATION_RESPONSE_FILE": inp.clarification_response_path,
696
813
  "CLARIFICATION_RESPONSE_RELATIVE_PATH": clarification_relative,
697
814
  "BRIEF_FILE_PATH": str(inp.brief_path),
698
815
  "BRIEF_RELATIVE_PATH": brief_relative,
699
- "LEAD_MODEL_DISPLAY": lead.display,
816
+ "LEAD_MODEL": lead.display,
700
817
  "LEAD_MODEL_EXECUTION_VALUE": lead.execution,
701
- "CLAUDE_WORKER_MODEL_DISPLAY": cw.display,
818
+ "CLAUDE_WORKER_MODEL": cw.display,
702
819
  "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
703
- "CODEX_WORKER_MODEL_DISPLAY": co.display,
820
+ "CODEX_WORKER_MODEL": co.display,
704
821
  "CODEX_WORKER_MODEL_EXECUTION_VALUE": co.execution,
705
- "GEMINI_WORKER_MODEL_DISPLAY": ge.display,
822
+ "GEMINI_WORKER_MODEL": ge.display,
706
823
  "GEMINI_WORKER_MODEL_EXECUTION_VALUE": ge.execution,
707
- "REPORT_WRITER_MODEL_DISPLAY": rw.display,
824
+ "REPORT_WRITER_MODEL": rw.display,
708
825
  "REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
709
826
  "EXECUTOR_PROVIDER": executor_provider,
710
827
  "EXECUTOR_DISPLAY_NAME": executor_display_name,
@@ -725,12 +842,39 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
725
842
  "OKSTRA_VERSION": installed_version(),
726
843
  **workflow_state,
727
844
  })
845
+ if inp.task_type == "implementation":
846
+ ctx["parsed_stage_map"] = ctx_stage_map
847
+ # Resolve effective stage and append `started` row to consumers.jsonl
848
+ from .consumers import read_consumers, append_consumer
849
+ import datetime as _dt
850
+ plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
851
+ consumed = read_consumers(plan_run_root)
852
+ done_stages = {r["stage"] for r in consumed if r.get("status") == "done"}
853
+ effective = _resolve_effective_stage(
854
+ ctx["parsed_stage_map"], done_stages, inp.stage
855
+ )
856
+ ctx["effective_stage"] = effective
857
+ inp.stage = str(effective)
858
+ print(f"selected stage: {inp.stage}", file=sys.stdout)
859
+ head_proc = _subprocess.run(
860
+ ["git", "rev-parse", "HEAD"],
861
+ cwd=inp.project_root, capture_output=True, text=True,
862
+ )
863
+ head_sha = head_proc.stdout.strip() if head_proc.returncode == 0 else ""
864
+ append_consumer(
865
+ plan_run_root,
866
+ impl_task_key=ctx["TASK_KEY"],
867
+ stage=effective,
868
+ status="started",
869
+ started_at=_dt.datetime.now(_dt.timezone.utc).isoformat(),
870
+ head_commit=head_sha,
871
+ )
728
872
 
729
873
  # ---- prepare directories + cleanup ----
730
874
  _ensure_task_directories(ctx)
731
875
  _migrate_legacy_run_artifacts(ctx)
732
876
  cleanup_obsolete_generated_docs(
733
- project_root=project_root, instruction_set_dir=Path(ctx["INSTRUCTION_SET_DIR"]),
877
+ project_root=project_root, instruction_set_dir=Path(ctx["INSTRUCTION_SET_PATH"]),
734
878
  )
735
879
  # Always materialise the resume command script. Even in --render-only
736
880
  # preparation flows the user (or a later non-interactive runner) may
@@ -738,12 +882,12 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
738
882
  # leaves runs/<phase>/sessions/ empty and the manifest pointing at a
739
883
  # path that does not exist.
740
884
  write_claude_resume_command_file(
741
- resume_command_path=Path(ctx["CLAUDE_RESUME_COMMAND_FILE"]),
885
+ resume_command_path=Path(ctx["CLAUDE_RESUME_COMMAND_PATH"]),
742
886
  project_root=project_root, claude_session_id=claude_session_id,
743
887
  )
744
888
 
745
889
  # ---- write instruction-set scaffolding ----
746
- instruction_set = Path(ctx["INSTRUCTION_SET_DIR"])
890
+ instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
747
891
  instruction_set.mkdir(parents=True, exist_ok=True)
748
892
  profile_rendered = profile_content
749
893
  for key in (
@@ -772,10 +916,17 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
772
916
  render_reference_expectations(
773
917
  str(inp.brief_path), str(instruction_set / "reference-expectations.md"), ctx,
774
918
  )
775
- render_template_file(
776
- str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_FILE"], ctx,
919
+ # inject populates ctx with compute + default tokens consumed by the lead
920
+ # prompt render below (claude-execution-prompt.md). The final-report
921
+ # template render is effectively a copy (Jinja2 `{{ var }}` syntax does
922
+ # not match `_TOKEN_RE`); routed through render_template_with_ctx for SOT
923
+ # consistency.
924
+ inject_lead_prompt_computed_tokens(ctx)
925
+ apply_lead_prompt_defaults(ctx)
926
+ render_template_with_ctx(
927
+ str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
777
928
  )
778
- render_template_file(
929
+ render_template_with_ctx(
779
930
  str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
780
931
  )
781
932
  prompt_text = (instruction_set / "claude-execution-prompt.md").read_text(encoding="utf-8")
@@ -810,12 +961,12 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
810
961
  if inp.render_only:
811
962
  ctx["CURRENT_TASK_STATUS"] = "instruction-set-generated"
812
963
  ctx["CURRENT_RUN_STATUS"] = "prepared"
813
- ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_FILE"]
964
+ ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
814
965
  ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
815
966
  else:
816
967
  ctx["CURRENT_TASK_STATUS"] = "claude-session-started"
817
968
  ctx["CURRENT_RUN_STATUS"] = "in-progress"
818
- ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_FILE"]
969
+ ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
819
970
  ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
820
971
  ctx.update(compute_workflow_state(
821
972
  task_type=inp.task_type,
@@ -824,11 +975,11 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
824
975
  render_only=inp.render_only,
825
976
  work_category=inp.work_category,
826
977
  ))
827
- render_team_state(ctx["TEAM_STATE_FILE"], ctx)
828
- render_task_manifest(ctx["TASK_MANIFEST_FILE"], ctx)
829
- render_task_index(str(task_index_template), ctx["TASK_INDEX_FILE"], ctx)
830
- render_run_manifest(ctx["RUN_MANIFEST_FILE"], ctx)
831
- render_timeline(ctx["TIMELINE_FILE"], ctx)
978
+ render_team_state(ctx["TEAM_STATE_PATH"], ctx)
979
+ render_task_manifest(ctx["TASK_MANIFEST_PATH"], ctx)
980
+ render_task_index(str(task_index_template), ctx["TASK_INDEX_PATH"], ctx)
981
+ render_run_manifest(ctx["RUN_MANIFEST_PATH"], ctx)
982
+ render_timeline(ctx["TIMELINE_PATH"], ctx)
832
983
  render_task_catalog_discovery(ctx["OKSTRA_TASK_CATALOG_FILE"], ctx)
833
984
  render_latest_task_discovery(ctx["OKSTRA_LATEST_TASK_FILE"], ctx)
834
985
 
@@ -867,6 +1018,31 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
867
1018
  file=__import__("sys").stderr,
868
1019
  )
869
1020
 
1021
+ try:
1022
+ claude_md_link = ensure_project_claude_md(project_root=Path(inp.project_root))
1023
+ except ClaudeMdLinkError as exc:
1024
+ print(
1025
+ f"okstra-claude-md: failed to provision project CLAUDE.md import — "
1026
+ f"Claude Code sessions in this project will not auto-load okstra guidance. ({exc})",
1027
+ file=__import__("sys").stderr,
1028
+ )
1029
+ else:
1030
+ if claude_md_link is None:
1031
+ print(
1032
+ "okstra-claude-md: ~/.okstra/templates/okstra.CLAUDE.md missing — "
1033
+ "re-run 'npx okstra@latest install' to provision the symlink target.",
1034
+ file=__import__("sys").stderr,
1035
+ )
1036
+
1037
+ try:
1038
+ ensure_project_agents_md(project_root=Path(inp.project_root))
1039
+ except AgentsMdLinkError as exc:
1040
+ print(
1041
+ f"okstra-agents-md: failed to provision <PROJECT>/AGENTS.md symlink — "
1042
+ f"codex / aider sessions in this project will not auto-load okstra guidance. ({exc})",
1043
+ file=__import__("sys").stderr,
1044
+ )
1045
+
870
1046
  return PrepareOutputs(
871
1047
  ctx=ctx,
872
1048
  prompt_text=prompt_text,
@@ -898,14 +1074,22 @@ def main(argv: list[str]) -> int:
898
1074
  p.add_argument("--executor", default="")
899
1075
  p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
900
1076
  p.add_argument("--approved-plan", default="", dest="approved_plan_path")
1077
+ p.add_argument(
1078
+ "--stage", default="auto", dest="stage",
1079
+ help=(
1080
+ "implementation task only. Which Stage Map entry to execute. "
1081
+ "'auto' (default) = lowest-numbered stage whose depends-on are all "
1082
+ "consumers.jsonl status:done. Numeric '<N>' = force that stage."
1083
+ ),
1084
+ )
901
1085
  p.add_argument(
902
1086
  "--approve",
903
1087
  action="store_true",
904
1088
  dest="approve_plan_ack",
905
1089
  help=(
906
1090
  "Treat the CLI invocation itself as the plan approval signal. "
907
- "Flips `- [ ] Approved` to `- [x] Approved` in the --approved-plan file "
908
- "and appends an audit line."
1091
+ "Flips `approved: false` to `approved: true` in the --approved-plan file's "
1092
+ "YAML frontmatter and appends an audit line."
909
1093
  ),
910
1094
  )
911
1095
  p.add_argument("--clarification-response", default="", dest="clarification_response_path")
@@ -992,6 +1176,7 @@ def main(argv: list[str]) -> int:
992
1176
  work_category=args.work_category,
993
1177
  base_ref=args.base_ref,
994
1178
  approved_plan_path=args.approved_plan_path,
1179
+ stage=args.stage,
995
1180
  clarification_response_path=clarification_abs,
996
1181
  pr_template_path=args.pr_template_path,
997
1182
  render_only=args.render_only,
@@ -1010,18 +1195,18 @@ def main(argv: list[str]) -> int:
1010
1195
  print(f"okstra task root: {ctx['TASK_ROOT']}")
1011
1196
  print(f"okstra latest task discovery file: {ctx['OKSTRA_LATEST_TASK_FILE']}")
1012
1197
  print(f"okstra task catalog file: {ctx['OKSTRA_TASK_CATALOG_FILE']}")
1013
- print(f"okstra instruction-set: {ctx['INSTRUCTION_SET_DIR']}")
1198
+ print(f"okstra instruction-set: {ctx['INSTRUCTION_SET_PATH']}")
1014
1199
  print(f"okstra reference expectations: {ctx['REFERENCE_EXPECTATIONS_FILE']}")
1015
- print(f"okstra final report template: {ctx['FINAL_REPORT_TEMPLATE_FILE']}")
1200
+ print(f"okstra final report template: {ctx['FINAL_REPORT_TEMPLATE_PATH']}")
1016
1201
  if inputs.render_only:
1017
1202
  print()
1018
1203
  print(out.prompt_text, end="")
1019
1204
  else:
1020
1205
  print(f"okstra current run dir: {ctx['RUN_DIR']}")
1021
- print(f"final report path: {ctx['FINAL_REPORT_FILE']}")
1022
- print(f"lead model: {ctx['LEAD_MODEL_DISPLAY']}")
1206
+ print(f"final report path: {ctx['FINAL_REPORT_PATH']}")
1207
+ print(f"lead model: {ctx['LEAD_MODEL']}")
1023
1208
  print(f"claude session id: {ctx['CLAUDE_SESSION_ID']}")
1024
- print(f"resume command file: {ctx['CLAUDE_RESUME_COMMAND_FILE']}")
1209
+ print(f"resume command file: {ctx['CLAUDE_RESUME_COMMAND_PATH']}")
1025
1210
  print("launch mode: interactive Claude handoff")
1026
1211
  print(f"claude working directory: {ctx['PROJECT_ROOT']}")
1027
1212
  print()
@@ -1031,7 +1216,7 @@ def main(argv: list[str]) -> int:
1031
1216
  "claudeSessionId": ctx["CLAUDE_SESSION_ID"],
1032
1217
  "leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
1033
1218
  "projectRoot": ctx["PROJECT_ROOT"],
1034
- "promptFile": str(Path(ctx["INSTRUCTION_SET_DIR"]) / "claude-execution-prompt.md"),
1219
+ "promptFile": str(Path(ctx["INSTRUCTION_SET_PATH"]) / "claude-execution-prompt.md"),
1035
1220
  }
1036
1221
  print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
1037
1222
  return 0