okstra 0.34.1 → 0.36.1

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 (108) hide show
  1. package/README.kr.md +27 -19
  2. package/README.md +27 -19
  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 +353 -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/plans/2026-05-24-implementation-lead-context-slimming.md +1700 -0
  17. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
  18. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
  19. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
  20. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
  21. package/docs/task-process/README.md +74 -0
  22. package/docs/task-process/common-flow.md +166 -0
  23. package/docs/task-process/error-analysis.md +101 -0
  24. package/docs/task-process/final-verification.md +167 -0
  25. package/docs/task-process/implementation-planning.md +128 -0
  26. package/docs/task-process/implementation.md +149 -0
  27. package/docs/task-process/release-handoff.md +206 -0
  28. package/docs/task-process/requirements-discovery.md +115 -0
  29. package/package.json +1 -1
  30. package/runtime/BUILD.json +2 -2
  31. package/runtime/agents/SKILL.md +30 -7
  32. package/runtime/agents/workers/claude-worker.md +31 -6
  33. package/runtime/agents/workers/codex-worker.md +37 -10
  34. package/runtime/agents/workers/gemini-worker.md +34 -7
  35. package/runtime/agents/workers/report-writer-worker.md +19 -10
  36. package/runtime/bin/okstra-central.sh +6 -6
  37. package/runtime/bin/okstra-codex-exec.sh +49 -28
  38. package/runtime/bin/okstra-gemini-exec.sh +39 -21
  39. package/runtime/bin/okstra-render-final-report.py +13 -2
  40. package/runtime/bin/okstra-wrapper-status.py +155 -0
  41. package/runtime/bin/okstra.sh +2 -2
  42. package/runtime/prompts/launch.template.md +1 -0
  43. package/runtime/prompts/profiles/_common-contract.md +11 -6
  44. package/runtime/prompts/profiles/_implementation-deliverable.md +53 -0
  45. package/runtime/prompts/profiles/_implementation-executor.md +60 -0
  46. package/runtime/prompts/profiles/_implementation-verifier.md +76 -0
  47. package/runtime/prompts/profiles/error-analysis.md +3 -7
  48. package/runtime/prompts/profiles/implementation-planning.md +22 -21
  49. package/runtime/prompts/profiles/implementation.md +28 -118
  50. package/runtime/prompts/profiles/improvement-discovery.md +42 -0
  51. package/runtime/prompts/profiles/release-handoff.md +1 -1
  52. package/runtime/prompts/profiles/requirements-discovery.md +8 -12
  53. package/runtime/prompts/wizard/prompts.ko.json +230 -0
  54. package/runtime/python/lib/okstra/cli.sh +2 -49
  55. package/runtime/python/lib/okstra/globals.sh +21 -21
  56. package/runtime/python/lib/okstra/interactive.sh +7 -7
  57. package/runtime/python/okstra_ctl/clarification_items.py +3 -9
  58. package/runtime/python/okstra_ctl/consumers.py +53 -0
  59. package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
  60. package/runtime/python/okstra_ctl/i18n.py +73 -0
  61. package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
  62. package/runtime/python/okstra_ctl/index.py +1 -1
  63. package/runtime/python/okstra_ctl/paths.py +26 -20
  64. package/runtime/python/okstra_ctl/render.py +166 -207
  65. package/runtime/python/okstra_ctl/render_final_report.py +53 -10
  66. package/runtime/python/okstra_ctl/run.py +299 -108
  67. package/runtime/python/okstra_ctl/run_context.py +22 -0
  68. package/runtime/python/okstra_ctl/seeding.py +186 -0
  69. package/runtime/python/okstra_ctl/session.py +65 -7
  70. package/runtime/python/okstra_ctl/wizard.py +348 -127
  71. package/runtime/python/okstra_ctl/workflow.py +21 -2
  72. package/runtime/python/okstra_ctl/worktree.py +54 -1
  73. package/runtime/python/okstra_project/resolver.py +4 -3
  74. package/runtime/python/okstra_token_usage/report.py +2 -2
  75. package/runtime/schemas/final-report-v1.0.schema.json +22 -16
  76. package/runtime/skills/okstra-brief/SKILL.md +102 -218
  77. package/runtime/skills/okstra-convergence/SKILL.md +2 -3
  78. package/runtime/skills/okstra-inspect/SKILL.md +581 -0
  79. package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
  80. package/runtime/skills/okstra-run/SKILL.md +8 -7
  81. package/runtime/skills/okstra-schedule/SKILL.md +14 -157
  82. package/runtime/skills/okstra-setup/SKILL.md +28 -1
  83. package/runtime/skills/okstra-team-contract/SKILL.md +16 -107
  84. package/runtime/templates/okstra.CLAUDE.md +104 -0
  85. package/runtime/templates/reports/brief.template.md +204 -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/schedule.template.md +12 -3
  92. package/runtime/templates/reports/task-brief.template.md +2 -2
  93. package/runtime/templates/worker-prompt-preamble.md +108 -0
  94. package/runtime/validators/lib/fixtures.sh +30 -0
  95. package/runtime/validators/lib/runners.sh +1 -1
  96. package/runtime/validators/validate-implementation-plan-stages.py +211 -0
  97. package/runtime/validators/validate-run.py +121 -26
  98. package/runtime/validators/validate-workflow.sh +2 -2
  99. package/runtime/validators/validate_improvement_report.py +275 -0
  100. package/src/config.mjs +18 -0
  101. package/src/install.mjs +41 -14
  102. package/src/setup.mjs +133 -1
  103. package/src/uninstall.mjs +27 -3
  104. package/runtime/skills/okstra-history/SKILL.md +0 -165
  105. package/runtime/skills/okstra-logs/SKILL.md +0 -173
  106. package/runtime/skills/okstra-report-finder/SKILL.md +0 -111
  107. package/runtime/skills/okstra-status/SKILL.md +0 -246
  108. package/runtime/skills/okstra-time-summary/SKILL.md +0 -172
@@ -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 로 토글.
182
282
 
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.
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.
290
+
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,18 @@ 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"]),
742
- project_root=project_root, claude_session_id=claude_session_id,
885
+ resume_command_path=Path(ctx["CLAUDE_RESUME_COMMAND_PATH"]),
886
+ project_root=project_root,
887
+ claude_session_id=claude_session_id,
888
+ task_key=ctx["TASK_KEY"],
889
+ task_type=ctx["TASK_TYPE"],
890
+ phase_state=ctx["CURRENT_RUN_STATUS"],
891
+ worker_prompts_dir_relative=ctx["RUN_PROMPTS_RELATIVE_PATH"],
892
+ prompt_seq=ctx["RUN_PROMPTS_SEQ"],
743
893
  )
744
894
 
745
895
  # ---- write instruction-set scaffolding ----
746
- instruction_set = Path(ctx["INSTRUCTION_SET_DIR"])
896
+ instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
747
897
  instruction_set.mkdir(parents=True, exist_ok=True)
748
898
  profile_rendered = profile_content
749
899
  for key in (
@@ -772,10 +922,17 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
772
922
  render_reference_expectations(
773
923
  str(inp.brief_path), str(instruction_set / "reference-expectations.md"), ctx,
774
924
  )
775
- render_template_file(
776
- str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_FILE"], ctx,
925
+ # inject populates ctx with compute + default tokens consumed by the lead
926
+ # prompt render below (claude-execution-prompt.md). The final-report
927
+ # template render is effectively a copy (Jinja2 `{{ var }}` syntax does
928
+ # not match `_TOKEN_RE`); routed through render_template_with_ctx for SOT
929
+ # consistency.
930
+ inject_lead_prompt_computed_tokens(ctx)
931
+ apply_lead_prompt_defaults(ctx)
932
+ render_template_with_ctx(
933
+ str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
777
934
  )
778
- render_template_file(
935
+ render_template_with_ctx(
779
936
  str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
780
937
  )
781
938
  prompt_text = (instruction_set / "claude-execution-prompt.md").read_text(encoding="utf-8")
@@ -810,12 +967,12 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
810
967
  if inp.render_only:
811
968
  ctx["CURRENT_TASK_STATUS"] = "instruction-set-generated"
812
969
  ctx["CURRENT_RUN_STATUS"] = "prepared"
813
- ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_FILE"]
970
+ ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
814
971
  ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
815
972
  else:
816
973
  ctx["CURRENT_TASK_STATUS"] = "claude-session-started"
817
974
  ctx["CURRENT_RUN_STATUS"] = "in-progress"
818
- ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_FILE"]
975
+ ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
819
976
  ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
820
977
  ctx.update(compute_workflow_state(
821
978
  task_type=inp.task_type,
@@ -824,11 +981,11 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
824
981
  render_only=inp.render_only,
825
982
  work_category=inp.work_category,
826
983
  ))
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)
984
+ render_team_state(ctx["TEAM_STATE_PATH"], ctx)
985
+ render_task_manifest(ctx["TASK_MANIFEST_PATH"], ctx)
986
+ render_task_index(str(task_index_template), ctx["TASK_INDEX_PATH"], ctx)
987
+ render_run_manifest(ctx["RUN_MANIFEST_PATH"], ctx)
988
+ render_timeline(ctx["TIMELINE_PATH"], ctx)
832
989
  render_task_catalog_discovery(ctx["OKSTRA_TASK_CATALOG_FILE"], ctx)
833
990
  render_latest_task_discovery(ctx["OKSTRA_LATEST_TASK_FILE"], ctx)
834
991
 
@@ -867,6 +1024,31 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
867
1024
  file=__import__("sys").stderr,
868
1025
  )
869
1026
 
1027
+ try:
1028
+ claude_md_link = ensure_project_claude_md(project_root=Path(inp.project_root))
1029
+ except ClaudeMdLinkError as exc:
1030
+ print(
1031
+ f"okstra-claude-md: failed to provision project CLAUDE.md import — "
1032
+ f"Claude Code sessions in this project will not auto-load okstra guidance. ({exc})",
1033
+ file=__import__("sys").stderr,
1034
+ )
1035
+ else:
1036
+ if claude_md_link is None:
1037
+ print(
1038
+ "okstra-claude-md: ~/.okstra/templates/okstra.CLAUDE.md missing — "
1039
+ "re-run 'npx okstra@latest install' to provision the symlink target.",
1040
+ file=__import__("sys").stderr,
1041
+ )
1042
+
1043
+ try:
1044
+ ensure_project_agents_md(project_root=Path(inp.project_root))
1045
+ except AgentsMdLinkError as exc:
1046
+ print(
1047
+ f"okstra-agents-md: failed to provision <PROJECT>/AGENTS.md symlink — "
1048
+ f"codex / aider sessions in this project will not auto-load okstra guidance. ({exc})",
1049
+ file=__import__("sys").stderr,
1050
+ )
1051
+
870
1052
  return PrepareOutputs(
871
1053
  ctx=ctx,
872
1054
  prompt_text=prompt_text,
@@ -898,14 +1080,22 @@ def main(argv: list[str]) -> int:
898
1080
  p.add_argument("--executor", default="")
899
1081
  p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
900
1082
  p.add_argument("--approved-plan", default="", dest="approved_plan_path")
1083
+ p.add_argument(
1084
+ "--stage", default="auto", dest="stage",
1085
+ help=(
1086
+ "implementation task only. Which Stage Map entry to execute. "
1087
+ "'auto' (default) = lowest-numbered stage whose depends-on are all "
1088
+ "consumers.jsonl status:done. Numeric '<N>' = force that stage."
1089
+ ),
1090
+ )
901
1091
  p.add_argument(
902
1092
  "--approve",
903
1093
  action="store_true",
904
1094
  dest="approve_plan_ack",
905
1095
  help=(
906
1096
  "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."
1097
+ "Flips `approved: false` to `approved: true` in the --approved-plan file's "
1098
+ "YAML frontmatter and appends an audit line."
909
1099
  ),
910
1100
  )
911
1101
  p.add_argument("--clarification-response", default="", dest="clarification_response_path")
@@ -992,6 +1182,7 @@ def main(argv: list[str]) -> int:
992
1182
  work_category=args.work_category,
993
1183
  base_ref=args.base_ref,
994
1184
  approved_plan_path=args.approved_plan_path,
1185
+ stage=args.stage,
995
1186
  clarification_response_path=clarification_abs,
996
1187
  pr_template_path=args.pr_template_path,
997
1188
  render_only=args.render_only,
@@ -1010,18 +1201,18 @@ def main(argv: list[str]) -> int:
1010
1201
  print(f"okstra task root: {ctx['TASK_ROOT']}")
1011
1202
  print(f"okstra latest task discovery file: {ctx['OKSTRA_LATEST_TASK_FILE']}")
1012
1203
  print(f"okstra task catalog file: {ctx['OKSTRA_TASK_CATALOG_FILE']}")
1013
- print(f"okstra instruction-set: {ctx['INSTRUCTION_SET_DIR']}")
1204
+ print(f"okstra instruction-set: {ctx['INSTRUCTION_SET_PATH']}")
1014
1205
  print(f"okstra reference expectations: {ctx['REFERENCE_EXPECTATIONS_FILE']}")
1015
- print(f"okstra final report template: {ctx['FINAL_REPORT_TEMPLATE_FILE']}")
1206
+ print(f"okstra final report template: {ctx['FINAL_REPORT_TEMPLATE_PATH']}")
1016
1207
  if inputs.render_only:
1017
1208
  print()
1018
1209
  print(out.prompt_text, end="")
1019
1210
  else:
1020
1211
  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']}")
1212
+ print(f"final report path: {ctx['FINAL_REPORT_PATH']}")
1213
+ print(f"lead model: {ctx['LEAD_MODEL']}")
1023
1214
  print(f"claude session id: {ctx['CLAUDE_SESSION_ID']}")
1024
- print(f"resume command file: {ctx['CLAUDE_RESUME_COMMAND_FILE']}")
1215
+ print(f"resume command file: {ctx['CLAUDE_RESUME_COMMAND_PATH']}")
1025
1216
  print("launch mode: interactive Claude handoff")
1026
1217
  print(f"claude working directory: {ctx['PROJECT_ROOT']}")
1027
1218
  print()
@@ -1031,7 +1222,7 @@ def main(argv: list[str]) -> int:
1031
1222
  "claudeSessionId": ctx["CLAUDE_SESSION_ID"],
1032
1223
  "leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
1033
1224
  "projectRoot": ctx["PROJECT_ROOT"],
1034
- "promptFile": str(Path(ctx["INSTRUCTION_SET_DIR"]) / "claude-execution-prompt.md"),
1225
+ "promptFile": str(Path(ctx["INSTRUCTION_SET_PATH"]) / "claude-execution-prompt.md"),
1035
1226
  }
1036
1227
  print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
1037
1228
  return 0