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
@@ -50,6 +50,15 @@ def _task_lock_path(task_key: str) -> Path:
50
50
  return locks / f"{safe}.lock"
51
51
 
52
52
 
53
+ def _consumers_lock_path(plan_task_key: str) -> Path:
54
+ """plan-task-key 별 consumers.jsonl append mutex."""
55
+ home = _okstra_home()
56
+ locks = home / ".locks"
57
+ locks.mkdir(parents=True, exist_ok=True)
58
+ safe = plan_task_key.replace("/", "_").replace(":", "_")
59
+ return locks / f"{safe}.consumers.lock"
60
+
61
+
53
62
  @contextmanager
54
63
  def task_mutex(task_key: str) -> Iterator[None]:
55
64
  """task-key per-process mutex. 동시 호출은 락 안에서 직렬화된다."""
@@ -63,6 +72,19 @@ def task_mutex(task_key: str) -> Iterator[None]:
63
72
  fcntl.flock(f.fileno(), fcntl.LOCK_UN)
64
73
 
65
74
 
75
+ @contextmanager
76
+ def consumers_mutex(plan_task_key: str) -> Iterator[None]:
77
+ """plan-task-key 별 consumers.jsonl append mutex."""
78
+ path = _consumers_lock_path(plan_task_key)
79
+ path.touch(exist_ok=True)
80
+ with path.open("r+") as f:
81
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX)
82
+ try:
83
+ yield
84
+ finally:
85
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
86
+
87
+
66
88
  def _atomic_write_json(path: Path, payload: dict) -> None:
67
89
  path.parent.mkdir(parents=True, exist_ok=True)
68
90
  tmp = path.with_suffix(path.suffix + ".tmp")
@@ -23,6 +23,14 @@ class SettingsLinkError(Exception):
23
23
  """`<project>/.claude/settings.local.json` symlink provisioning 실패."""
24
24
 
25
25
 
26
+ class ClaudeMdLinkError(Exception):
27
+ """`<project>/.project-docs/okstra/CLAUDE.md` symlink or import-block provisioning 실패."""
28
+
29
+
30
+ class AgentsMdLinkError(Exception):
31
+ """`<project>/AGENTS.md` symlink provisioning 실패."""
32
+
33
+
26
34
  def installed_version() -> str:
27
35
  """Read the version stamp written by `okstra install` to `~/.okstra/version`.
28
36
 
@@ -195,3 +203,181 @@ def _backup_and_replace(target: Path, template: Path) -> None:
195
203
  raise SettingsLinkError(
196
204
  f"failed to create symlink {target} -> {template} after backup: {exc}"
197
205
  ) from exc
206
+
207
+
208
+ def installed_claude_md_template_path() -> Path:
209
+ """okstra install 이 만들어 둔 okstra.CLAUDE.md template 의 절대경로."""
210
+ return _okstra_home() / "templates" / "okstra.CLAUDE.md"
211
+
212
+
213
+ _CLAUDE_MD_SYMLINK_REL = Path(".project-docs") / "okstra" / "CLAUDE.md"
214
+ _CLAUDE_MD_IMPORT_LINE = "@.project-docs/okstra/CLAUDE.md"
215
+ _CLAUDE_MD_MARKER_BEGIN = (
216
+ "<!-- okstra:claude-md:begin (managed by okstra setup — do not edit) -->"
217
+ )
218
+ _CLAUDE_MD_MARKER_END = "<!-- okstra:claude-md:end -->"
219
+
220
+
221
+ def ensure_project_claude_md(*, project_root: Path) -> Optional[Path]:
222
+ """`<project_root>/.project-docs/okstra/CLAUDE.md` 를 `~/.okstra/templates/okstra.CLAUDE.md`
223
+ 로 가리키는 symlink 로 provisioning 하고, `<project_root>/CLAUDE.md` 에
224
+ `@.project-docs/okstra/CLAUDE.md` import block 을 멱등하게 주입한다.
225
+
226
+ Claude Code 가 해당 프로젝트에서 host 세션으로 실행될 때
227
+ `<project_root>/CLAUDE.md` 가 자동 로드되므로, okstra 가 관리하는 본문
228
+ (slash command catalog, workflow guidance, ...) 도 같이 surface 된다.
229
+
230
+ 반환값:
231
+ - symlink Path: 신규 생성됐거나 이미 올바른 target 을 가리키고 있을 때.
232
+ - None: install 이 아직 CLAUDE.md template 을 깔지 않았을 때 (구버전
233
+ okstra install). 상위에서 경고로 흘려보낸다.
234
+
235
+ 상위 호출자는 `ClaudeMdLinkError` 만 처리하면 된다.
236
+ """
237
+ project_root = Path(project_root)
238
+ template = installed_claude_md_template_path()
239
+ if not template.exists():
240
+ return None
241
+
242
+ target = project_root / _CLAUDE_MD_SYMLINK_REL
243
+ target.parent.mkdir(parents=True, exist_ok=True)
244
+
245
+ if target.is_symlink():
246
+ try:
247
+ current = os.readlink(target)
248
+ except OSError as exc:
249
+ raise ClaudeMdLinkError(
250
+ f"failed to read existing symlink {target}: {exc}"
251
+ ) from exc
252
+ current_path = Path(current)
253
+ if current_path == template or (target.parent / current_path).resolve() == template.resolve():
254
+ _ensure_claude_md_import(project_root)
255
+ return target
256
+ _backup_and_replace_claude_md(target, template)
257
+ _ensure_claude_md_import(project_root)
258
+ return target
259
+
260
+ if target.exists():
261
+ _backup_and_replace_claude_md(target, template)
262
+ _ensure_claude_md_import(project_root)
263
+ return target
264
+
265
+ try:
266
+ target.symlink_to(template)
267
+ except OSError as exc:
268
+ raise ClaudeMdLinkError(
269
+ f"failed to create symlink {target} -> {template}: {exc}"
270
+ ) from exc
271
+ _ensure_claude_md_import(project_root)
272
+ return target
273
+
274
+
275
+ def _ensure_claude_md_import(project_root: Path) -> bool:
276
+ """`<project_root>/CLAUDE.md` 에 import block 이 없으면 append, 있으면 no-op.
277
+
278
+ 반환: 새로 주입했을 때 True, 이미 있었을 때 False.
279
+ """
280
+ claude_md = project_root / "CLAUDE.md"
281
+ block = f"{_CLAUDE_MD_MARKER_BEGIN}\n{_CLAUDE_MD_IMPORT_LINE}\n{_CLAUDE_MD_MARKER_END}\n"
282
+
283
+ try:
284
+ existing = claude_md.read_text(encoding="utf-8")
285
+ except FileNotFoundError:
286
+ try:
287
+ claude_md.write_text(block, encoding="utf-8")
288
+ except OSError as exc:
289
+ raise ClaudeMdLinkError(
290
+ f"failed to create {claude_md}: {exc}"
291
+ ) from exc
292
+ return True
293
+ except OSError as exc:
294
+ raise ClaudeMdLinkError(f"failed to read {claude_md}: {exc}") from exc
295
+
296
+ if _CLAUDE_MD_MARKER_BEGIN in existing and _CLAUDE_MD_MARKER_END in existing:
297
+ return False
298
+
299
+ separator = _block_separator_for(existing)
300
+ try:
301
+ claude_md.write_text(existing + separator + block, encoding="utf-8")
302
+ except OSError as exc:
303
+ raise ClaudeMdLinkError(f"failed to update {claude_md}: {exc}") from exc
304
+ return True
305
+
306
+
307
+ def _block_separator_for(existing: str) -> str:
308
+ """기존 파일 끝과 import block 사이의 공백 결정 — 가독성 보장 목적."""
309
+ if not existing:
310
+ return ""
311
+ if existing.endswith("\n\n"):
312
+ return ""
313
+ if existing.endswith("\n"):
314
+ return "\n"
315
+ return "\n\n"
316
+
317
+
318
+ def _backup_and_replace_claude_md(target: Path, template: Path) -> None:
319
+ """기존 파일/심볼릭링크를 timestamped backup 으로 옮기고 새 symlink 생성."""
320
+ stamp = time.strftime("%Y%m%d-%H%M%S")
321
+ backup = target.with_name(f"{target.name}.bak.{stamp}")
322
+ try:
323
+ target.rename(backup)
324
+ except OSError as exc:
325
+ raise ClaudeMdLinkError(
326
+ f"failed to back up existing {target} to {backup}: {exc}"
327
+ ) from exc
328
+ try:
329
+ target.symlink_to(template)
330
+ except OSError as exc:
331
+ raise ClaudeMdLinkError(
332
+ f"failed to create symlink {target} -> {template} after backup: {exc}"
333
+ ) from exc
334
+
335
+
336
+ def ensure_project_agents_md(*, project_root: Path) -> Optional[Path]:
337
+ """`<project_root>/AGENTS.md` 가 없을 때만 `~/.okstra/templates/okstra.CLAUDE.md`
338
+ 로의 심링크로 생성한다.
339
+
340
+ AGENTS.md 는 codex / aider / 기타 agent 가 읽는 파일이고 @import 같은
341
+ 부분 포함 메커니즘이 없어, 파일 전체 내용이 곧 agent 가 보는 콘텐츠가
342
+ 된다. 그래서 CLAUDE.md (`@.project-docs/okstra/CLAUDE.md` 마커 블록
343
+ 주입) 와 달리 "AGENTS.md 가 비어 있을 때만 만들고, 존재하면 절대
344
+ 건드리지 않는" 정책을 사용한다 — 사용자가 직접 작성한 AGENTS.md 를
345
+ 덮어쓰지 않는다.
346
+
347
+ 반환값:
348
+ - target Path: 신규 심링크 생성, 또는 이미 우리 템플릿을 가리키고
349
+ 있던 심링크가 idempotent 하게 확인된 경우.
350
+ - None: install 이 아직 CLAUDE.md template 을 깔지 않았거나,
351
+ AGENTS.md 가 이미 사용자 콘텐츠 (regular file) 거나 우리가
352
+ 만들지 않은 다른 심링크로 존재하는 경우. 후자 두 케이스는
353
+ 사용자의 의도로 간주하고 건드리지 않는다.
354
+
355
+ 상위 호출자는 `AgentsMdLinkError` 만 처리하면 된다.
356
+ """
357
+ project_root = Path(project_root)
358
+ template = installed_claude_md_template_path()
359
+ if not template.exists():
360
+ return None
361
+
362
+ target = project_root / "AGENTS.md"
363
+
364
+ if target.is_symlink():
365
+ try:
366
+ current = os.readlink(target)
367
+ except OSError:
368
+ return None
369
+ current_path = Path(current)
370
+ if current_path == template or (target.parent / current_path).resolve() == template.resolve():
371
+ return target
372
+ return None # foreign symlink — respect user
373
+
374
+ if target.exists():
375
+ return None # regular file — respect user content
376
+
377
+ try:
378
+ target.symlink_to(template)
379
+ except OSError as exc:
380
+ raise AgentsMdLinkError(
381
+ f"failed to create symlink {target} -> {template}: {exc}"
382
+ ) from exc
383
+ return target
@@ -49,18 +49,76 @@ def resolve_inproc_lead_session_id(project_root: Path) -> str:
49
49
 
50
50
 
51
51
  def write_claude_resume_command_file(
52
- *, resume_command_path: Path, project_root: Path, claude_session_id: str,
52
+ *,
53
+ resume_command_path: Path,
54
+ project_root: Path,
55
+ claude_session_id: str,
56
+ task_key: str,
57
+ task_type: str,
58
+ phase_state: str,
59
+ worker_prompts_dir_relative: str,
60
+ prompt_seq: str,
53
61
  ) -> None:
54
62
  """`bash claude-resume-*.sh` 를 실행하면 task 의 claude 세션을 resume
55
63
  하도록 shell 스크립트를 작성하고 chmod +x.
64
+
65
+ `claude --resume` 자체는 직전 세션 context 만 복구하고 lead 는
66
+ 사용자 입력 대기 상태로 멈춘다. 따라서 사용자가 resume 후 무엇을
67
+ 입력해야 하는지를 안내하는 guidance 블록을 sh 안에 inline 한다 —
68
+ sh 가 실행될 때 worker prompt 디스크 존재 여부를 직접 검사해
69
+ Phase 2 부터 / Phase 3 부터 중 알맞은 다음 명령을 추천한다.
56
70
  """
57
71
  resume_command_path = Path(resume_command_path)
58
72
  resume_command_path.parent.mkdir(parents=True, exist_ok=True)
59
- body = (
60
- "#!/usr/bin/env bash\n"
61
- "# Generated by okstra. Resume the prepared Claude session for this run.\n"
62
- f'cd "{project_root}"\n'
63
- f'exec claude --resume "{claude_session_id}"\n'
64
- )
73
+ body = f"""#!/usr/bin/env bash
74
+ # Generated by okstra. Resume the prepared Claude session for this run.
75
+
76
+ TASK_KEY={_sh_single_quote(task_key)}
77
+ TASK_TYPE={_sh_single_quote(task_type)}
78
+ PHASE_STATE={_sh_single_quote(phase_state)}
79
+ PROJECT_ROOT={_sh_single_quote(str(project_root))}
80
+ WORKER_PROMPTS_DIR="$PROJECT_ROOT/{worker_prompts_dir_relative}"
81
+ PROMPT_SEQ={_sh_single_quote(prompt_seq)}
82
+ SESSION_ID={_sh_single_quote(claude_session_id)}
83
+
84
+ cat >&2 <<EOF
85
+ ============================================================
86
+ okstra resume — $TASK_KEY
87
+ Phase: $TASK_TYPE ($PHASE_STATE)
88
+ ============================================================
89
+
90
+ 이 스크립트는 Claude 세션 context 만 복구합니다.
91
+ Lead 는 resume 직후 자동으로 진행하지 않으니, 첫 메시지로 다음을
92
+ 그대로 (또는 상황에 맞게 수정해서) 입력하세요:
93
+
94
+ EOF
95
+
96
+ if compgen -G "$WORKER_PROMPTS_DIR/*-worker-prompt-$TASK_TYPE-$PROMPT_SEQ.md" > /dev/null 2>&1; then
97
+ cat >&2 <<EOF
98
+ Phase 3 부터 진행 — TeamCreate 후 Phase 4 worker dispatch 까지.
99
+ Worker prompts (이미 작성·저장됨):
100
+ $WORKER_PROMPTS_DIR/*-worker-prompt-$TASK_TYPE-$PROMPT_SEQ.md
101
+ EOF
102
+ else
103
+ cat >&2 <<EOF
104
+ Phase 2 부터 진행 — worker prompts 작성·저장 후 Phase 3 진행.
105
+ Prompts directory (현재 비어 있음):
106
+ $WORKER_PROMPTS_DIR
107
+ EOF
108
+ fi
109
+
110
+ cat >&2 <<EOF
111
+
112
+ ============================================================
113
+ EOF
114
+
115
+ cd "$PROJECT_ROOT"
116
+ exec claude --resume "$SESSION_ID"
117
+ """
65
118
  resume_command_path.write_text(body, encoding="utf-8")
66
119
  os.chmod(resume_command_path, 0o755)
120
+
121
+
122
+ def _sh_single_quote(value: str) -> str:
123
+ """POSIX-safe single-quote: `it's` → `'it'"'"'s'`."""
124
+ return "'" + value.replace("'", "'\"'\"'") + "'"