okstra 0.1.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 (106) hide show
  1. package/README.md +36 -0
  2. package/bin/okstra +62 -0
  3. package/package.json +30 -0
  4. package/runtime/.gitkeep +0 -0
  5. package/runtime/BUILD.json +5 -0
  6. package/runtime/agents/SKILL.md +243 -0
  7. package/runtime/agents/TODO.md +168 -0
  8. package/runtime/agents/workers/claude-worker.md +106 -0
  9. package/runtime/agents/workers/codex-worker.md +179 -0
  10. package/runtime/agents/workers/gemini-worker.md +179 -0
  11. package/runtime/agents/workers/report-writer-worker.md +116 -0
  12. package/runtime/bin/okstra-central.sh +152 -0
  13. package/runtime/bin/okstra-codex-exec.sh +53 -0
  14. package/runtime/bin/okstra-error-log.py +295 -0
  15. package/runtime/bin/okstra-gemini-exec.sh +55 -0
  16. package/runtime/bin/okstra-token-usage.py +46 -0
  17. package/runtime/bin/okstra.sh +162 -0
  18. package/runtime/prompts/launch.template.md +52 -0
  19. package/runtime/prompts/profiles/error-analysis.md +43 -0
  20. package/runtime/prompts/profiles/final-verification.md +37 -0
  21. package/runtime/prompts/profiles/implementation-planning.md +85 -0
  22. package/runtime/prompts/profiles/implementation.md +71 -0
  23. package/runtime/prompts/profiles/requirements-discovery.md +43 -0
  24. package/runtime/python/lib/okstra/cli.sh +227 -0
  25. package/runtime/python/lib/okstra/globals.sh +157 -0
  26. package/runtime/python/lib/okstra/interactive.sh +411 -0
  27. package/runtime/python/lib/okstra/project-resolver.sh +57 -0
  28. package/runtime/python/lib/okstra/usage.sh +98 -0
  29. package/runtime/python/lib/okstra-ctl/cmd-batch.sh +59 -0
  30. package/runtime/python/lib/okstra-ctl/cmd-list.sh +35 -0
  31. package/runtime/python/lib/okstra-ctl/cmd-open.sh +36 -0
  32. package/runtime/python/lib/okstra-ctl/cmd-projects.sh +26 -0
  33. package/runtime/python/lib/okstra-ctl/cmd-reconcile.sh +27 -0
  34. package/runtime/python/lib/okstra-ctl/cmd-reindex.sh +38 -0
  35. package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +326 -0
  36. package/runtime/python/lib/okstra-ctl/cmd-show.sh +27 -0
  37. package/runtime/python/lib/okstra-ctl/cmd-tail.sh +76 -0
  38. package/runtime/python/lib/okstra-ctl/main.sh +41 -0
  39. package/runtime/python/lib/okstra-ctl/prepare.sh +29 -0
  40. package/runtime/python/lib/okstra-ctl/usage.sh +23 -0
  41. package/runtime/python/okstra_ctl/__init__.py +125 -0
  42. package/runtime/python/okstra_ctl/backfill.py +253 -0
  43. package/runtime/python/okstra_ctl/batch.py +62 -0
  44. package/runtime/python/okstra_ctl/ids.py +84 -0
  45. package/runtime/python/okstra_ctl/index.py +216 -0
  46. package/runtime/python/okstra_ctl/invocation.py +49 -0
  47. package/runtime/python/okstra_ctl/jsonl.py +84 -0
  48. package/runtime/python/okstra_ctl/listing.py +156 -0
  49. package/runtime/python/okstra_ctl/locks.py +42 -0
  50. package/runtime/python/okstra_ctl/material.py +62 -0
  51. package/runtime/python/okstra_ctl/models.py +63 -0
  52. package/runtime/python/okstra_ctl/path_resolve.py +40 -0
  53. package/runtime/python/okstra_ctl/paths.py +251 -0
  54. package/runtime/python/okstra_ctl/project_meta.py +51 -0
  55. package/runtime/python/okstra_ctl/reconcile.py +166 -0
  56. package/runtime/python/okstra_ctl/render.py +1065 -0
  57. package/runtime/python/okstra_ctl/resolver.py +54 -0
  58. package/runtime/python/okstra_ctl/run.py +674 -0
  59. package/runtime/python/okstra_ctl/run_context.py +166 -0
  60. package/runtime/python/okstra_ctl/seeding.py +97 -0
  61. package/runtime/python/okstra_ctl/sequence.py +53 -0
  62. package/runtime/python/okstra_ctl/session.py +33 -0
  63. package/runtime/python/okstra_ctl/tmux.py +27 -0
  64. package/runtime/python/okstra_ctl/workers.py +64 -0
  65. package/runtime/python/okstra_ctl/workflow.py +182 -0
  66. package/runtime/python/okstra_project/__init__.py +41 -0
  67. package/runtime/python/okstra_project/resolver.py +126 -0
  68. package/runtime/python/okstra_project/state.py +170 -0
  69. package/runtime/python/okstra_token_usage/__init__.py +26 -0
  70. package/runtime/python/okstra_token_usage/blocks.py +62 -0
  71. package/runtime/python/okstra_token_usage/claude.py +97 -0
  72. package/runtime/python/okstra_token_usage/cli.py +84 -0
  73. package/runtime/python/okstra_token_usage/codex.py +80 -0
  74. package/runtime/python/okstra_token_usage/collect.py +161 -0
  75. package/runtime/python/okstra_token_usage/gemini.py +77 -0
  76. package/runtime/python/okstra_token_usage/jsonl_io.py +18 -0
  77. package/runtime/python/okstra_token_usage/paths.py +22 -0
  78. package/runtime/python/okstra_token_usage/pricing.py +71 -0
  79. package/runtime/python/okstra_token_usage/report.py +64 -0
  80. package/runtime/templates/prd/brief.template.md +273 -0
  81. package/runtime/templates/project-docs/task-index.template.md +65 -0
  82. package/runtime/templates/reports/error-analysis-input.template.md +80 -0
  83. package/runtime/templates/reports/final-report.template.md +167 -0
  84. package/runtime/templates/reports/final-verification-input.template.md +67 -0
  85. package/runtime/templates/reports/implementation-input.template.md +81 -0
  86. package/runtime/templates/reports/implementation-planning-input.template.md +93 -0
  87. package/runtime/templates/reports/quick-input.template.md +64 -0
  88. package/runtime/templates/reports/schedule.template.md +168 -0
  89. package/runtime/templates/reports/settings.template.json +101 -0
  90. package/runtime/templates/reports/task-brief.template.md +165 -0
  91. package/runtime/validators/lib/common.sh +44 -0
  92. package/runtime/validators/lib/fixtures.sh +322 -0
  93. package/runtime/validators/lib/paths.sh +44 -0
  94. package/runtime/validators/lib/runners.sh +140 -0
  95. package/runtime/validators/lib/summary.sh +15 -0
  96. package/runtime/validators/lib/validate-assets.sh +44 -0
  97. package/runtime/validators/lib/validate-prompt-metadata.sh +267 -0
  98. package/runtime/validators/lib/validate-tasks.sh +335 -0
  99. package/runtime/validators/validate-run.py +568 -0
  100. package/runtime/validators/validate-schedule.py +665 -0
  101. package/runtime/validators/validate-workflow.sh +190 -0
  102. package/src/doctor.mjs +127 -0
  103. package/src/install.mjs +355 -0
  104. package/src/paths.mjs +132 -0
  105. package/src/uninstall.mjs +122 -0
  106. package/src/version.mjs +20 -0
@@ -0,0 +1,166 @@
1
+ """run-scoped state 의 file-based 저장/조회.
2
+
3
+ 설계 원칙:
4
+ - 한 run 의 입력 (`task-type`, `brief-path`, `directive`, `workers`, `models`,
5
+ `related-tasks`, `approved-plan`, `clarification-response`) 은
6
+ `<run-dir>/manifests/run-inputs-<seq>.json` 에 박힌다.
7
+ - 한 run 의 계산된 paths/seqs/timestamp 는
8
+ `<run-dir>/manifests/run-context-<seq>.json` 에 박힌다.
9
+ - 두 파일이 한 번 디스크에 자리잡으면, 이후 모든 reader 는 환경 변수에 의존
10
+ 하지 않고 같은 값을 돌려준다 → 같은 claude 세션의 두 병렬 호출이 stale
11
+ snapshot 을 보지 않는다.
12
+
13
+ 부수효과 함수는 모두 per-task 락 (`~/.okstra/.locks/<task-key>.lock`) 안에서만
14
+ seq advance 와 파일 쓰기를 수행한다.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import fcntl
19
+ import json
20
+ import os
21
+ from contextlib import contextmanager
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Iterator, Optional
25
+
26
+ from .paths import compute_run_paths
27
+
28
+
29
+ def _now_iso() -> str:
30
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
31
+
32
+
33
+ def _okstra_home() -> Path:
34
+ home = os.environ.get("OKSTRA_HOME")
35
+ if home:
36
+ return Path(home)
37
+ return Path.home() / ".okstra"
38
+
39
+
40
+ def _task_lock_path(task_key: str) -> Path:
41
+ """task-key 별 mutex 파일. central index 와는 별개로 task 단위 직렬화."""
42
+ home = _okstra_home()
43
+ locks = home / ".locks"
44
+ locks.mkdir(parents=True, exist_ok=True)
45
+ safe = task_key.replace("/", "_").replace(":", "_")
46
+ return locks / f"{safe}.lock"
47
+
48
+
49
+ @contextmanager
50
+ def task_mutex(task_key: str) -> Iterator[None]:
51
+ """task-key per-process mutex. 동시 호출은 락 안에서 직렬화된다."""
52
+ path = _task_lock_path(task_key)
53
+ path.touch(exist_ok=True)
54
+ with path.open("r+") as f:
55
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX)
56
+ try:
57
+ yield
58
+ finally:
59
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
60
+
61
+
62
+ def _atomic_write_json(path: Path, payload: dict) -> None:
63
+ path.parent.mkdir(parents=True, exist_ok=True)
64
+ tmp = path.with_suffix(path.suffix + ".tmp")
65
+ tmp.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
66
+ encoding="utf-8")
67
+ os.replace(tmp, path)
68
+
69
+
70
+ def _run_context_filename(task_type_segment: str, seq: str) -> str:
71
+ return f"run-context-{task_type_segment}-{seq}.json"
72
+
73
+
74
+ def _run_inputs_filename(task_type_segment: str, seq: str) -> str:
75
+ return f"run-inputs-{task_type_segment}-{seq}.json"
76
+
77
+
78
+ def compute_and_write_run_context(
79
+ *,
80
+ workspace_root: Path,
81
+ project_root: Path,
82
+ project_id: str,
83
+ task_group: str,
84
+ task_id: str,
85
+ task_type: str,
86
+ run_seq_override: Optional[int] = None,
87
+ ) -> dict:
88
+ """task per-mutex 안에서 run paths 를 계산하고 디스크에 박는다.
89
+
90
+ 기존 next_run_seq 가 디스크 스캔 기반이라 두 호출이 동시에 진행되면 같은
91
+ seq 를 받을 수 있다. mutex 안에서 (a) seq 계산 (b) run-context.json 저장
92
+ 이 한 트랜잭션으로 묶이면, 다음 호출은 새로 박힌 파일을 보고 다음 seq 를
93
+ 돌려준다. 락은 OKSTRA_HOME/.locks/<task-key>.lock.
94
+
95
+ 반환값: paths dict + 부가 메타(`runTimestamp`, 저장된 파일 경로 등).
96
+ """
97
+ project_root = Path(project_root)
98
+ workspace_root = Path(workspace_root)
99
+ task_key = f"{project_id}:{task_group}:{task_id}"
100
+
101
+ with task_mutex(task_key):
102
+ ctx = compute_run_paths(
103
+ project_root=project_root,
104
+ workspace_root=workspace_root,
105
+ project_id=project_id,
106
+ task_group=task_group,
107
+ task_id=task_id,
108
+ task_type=task_type,
109
+ run_seq_override=run_seq_override,
110
+ )
111
+ ctx["RUN_TIMESTAMP_ISO"] = _now_iso()
112
+ run_manifests_dir = Path(ctx["RUN_MANIFESTS_DIR"])
113
+ ctx_path = run_manifests_dir / _run_context_filename(
114
+ ctx["TASK_TYPE_SEGMENT"], ctx["RUN_MANIFESTS_SEQ"])
115
+ ctx["RUN_CONTEXT_FILE"] = str(ctx_path)
116
+ ctx["RUN_CONTEXT_RELATIVE_PATH"] = (
117
+ str(ctx_path.relative_to(project_root.resolve()))
118
+ if ctx_path.resolve().is_relative_to(project_root.resolve())
119
+ else str(ctx_path))
120
+ _atomic_write_json(ctx_path, ctx)
121
+ return ctx
122
+
123
+
124
+ def write_run_inputs(
125
+ *,
126
+ project_root: Path,
127
+ run_manifests_dir: Path,
128
+ task_type_segment: str,
129
+ seq: str,
130
+ inputs: dict,
131
+ ) -> Path:
132
+ """사용자 입력(brief, directive, workers, models, ...) 을 run-inputs 파일에
133
+ 박는다. 호출자가 미리 계산한 seq 를 그대로 사용한다(같은 트랜잭션 내).
134
+
135
+ inputs schema (모든 키 optional):
136
+ taskBriefPath, directive, workers, leadModel, claudeModel, codexModel,
137
+ geminiModel, reportWriterModel, relatedTasks, approvedPlanPath,
138
+ clarificationResponsePath, renderOnly, refreshAssets
139
+ """
140
+ run_manifests_dir = Path(run_manifests_dir)
141
+ path = run_manifests_dir / _run_inputs_filename(task_type_segment, seq)
142
+ payload = {
143
+ "schemaVersion": "1.0",
144
+ "writtenAt": _now_iso(),
145
+ "inputs": dict(inputs),
146
+ }
147
+ _atomic_write_json(path, payload)
148
+ return path
149
+
150
+
151
+ def read_run_context(run_manifests_dir: Path, task_type_segment: str,
152
+ seq: str) -> Optional[dict]:
153
+ """run-context-<task-type>-<seq>.json 을 dict 로 돌려준다. 부재 시 None."""
154
+ path = Path(run_manifests_dir) / _run_context_filename(task_type_segment, seq)
155
+ if not path.is_file():
156
+ return None
157
+ return json.loads(path.read_text(encoding="utf-8"))
158
+
159
+
160
+ def read_run_inputs(run_manifests_dir: Path, task_type_segment: str,
161
+ seq: str) -> Optional[dict]:
162
+ """run-inputs-<task-type>-<seq>.json 을 dict 로. 부재 시 None."""
163
+ path = Path(run_manifests_dir) / _run_inputs_filename(task_type_segment, seq)
164
+ if not path.is_file():
165
+ return None
166
+ return json.loads(path.read_text(encoding="utf-8"))
@@ -0,0 +1,97 @@
1
+ """okstra runtime asset verification + per-run runtime settings.
2
+
3
+ okstra 가 깔아둔 런타임(`~/.okstra/lib/python`, `~/.okstra/bin`,
4
+ `~/.okstra/version`) 이 있는지 확인하고, 누락 시 InstallationError 로
5
+ surface 한다. 또한 claude 런치 때 사용할 휘발성 settings 파일을 현재 run
6
+ 디렉토리에 만든다.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+
15
+ class InstallationError(Exception):
16
+ """okstra 가 깔아둔 런타임 자산이 누락됨."""
17
+
18
+
19
+ def required_install_paths() -> list[Path]:
20
+ """okstra install 이 채워야 하는 최소 자산 경로."""
21
+ okstra_home = Path.home() / ".okstra"
22
+ return [
23
+ okstra_home / "lib" / "python" / "okstra_project",
24
+ okstra_home / "lib" / "python" / "okstra_ctl",
25
+ okstra_home / "bin" / "okstra.sh",
26
+ okstra_home / "version",
27
+ ]
28
+
29
+
30
+ def verify_installation(workspace_root: Path) -> None:
31
+ """누락된 자산이 있으면 `InstallationError` 를 raise. 메시지에 install
32
+ 명령을 포함한다.
33
+
34
+ workspace_root 는 prompts/, templates/, validators/, agents/ 를 담은
35
+ 디렉터리(`<okstra-package>/runtime` 또는 dev-link 모드의 repo 루트)다.
36
+ 이 검사는 ~/.okstra 자산만을 본다; workspace_root 의 존재는 별도 검증.
37
+ """
38
+ missing = [p for p in required_install_paths() if not p.exists()]
39
+ if missing:
40
+ msg_lines = [f"okstra runtime missing: {p}" for p in missing]
41
+ msg_lines.append("")
42
+ msg_lines.append("okstra has not been installed yet. Run once:")
43
+ msg_lines.append(" npx okstra@latest install")
44
+ raise InstallationError("\n".join(msg_lines))
45
+
46
+ workspace_root = Path(workspace_root)
47
+ if not workspace_root.is_dir():
48
+ raise InstallationError(
49
+ f"okstra workspace not found: {workspace_root}\n"
50
+ "Ensure 'okstra paths --field workspace' resolves to an existing directory."
51
+ )
52
+
53
+
54
+ def cleanup_obsolete_generated_docs(
55
+ *, project_root: Path, instruction_set_dir: Path,
56
+ ) -> None:
57
+ """과거 위치에 남아 있을 수 있는 deprecated 문서들을 best-effort 삭제."""
58
+ project_root = Path(project_root)
59
+ legacy_root = project_root / ".project-docs" / "ai"
60
+ for rel in (
61
+ "claude-skill-index.md",
62
+ "okstra/okstra-guide.md",
63
+ "okstra/worker-catalog.md",
64
+ ):
65
+ target = legacy_root / rel
66
+ if target.exists():
67
+ try:
68
+ target.unlink()
69
+ except OSError:
70
+ pass
71
+ obsolete = Path(instruction_set_dir) / "okstra-skill.md"
72
+ if obsolete.exists():
73
+ try:
74
+ obsolete.unlink()
75
+ except OSError:
76
+ pass
77
+ okstra_dir = legacy_root / "okstra"
78
+ if okstra_dir.is_dir():
79
+ try:
80
+ okstra_dir.rmdir()
81
+ except OSError:
82
+ pass
83
+
84
+
85
+ def render_runtime_settings_file(
86
+ *, workspace_root: Path, run_dir: Path,
87
+ ) -> Optional[Path]:
88
+ """`templates/reports/settings.template.json` 을 `<run-dir>/okstra-runtime-settings.json`
89
+ 으로 복사한다. 템플릿 부재 시 None 반환(상위에서 `--settings` 인자 skip).
90
+ """
91
+ template = Path(workspace_root) / "templates" / "reports" / "settings.template.json"
92
+ if not template.is_file():
93
+ return None
94
+ target = Path(run_dir) / "okstra-runtime-settings.json"
95
+ target.parent.mkdir(parents=True, exist_ok=True)
96
+ shutil.copyfile(template, target)
97
+ return target
@@ -0,0 +1,53 @@
1
+ """run_seq 예측."""
2
+ from __future__ import annotations
3
+
4
+ import re as _re
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from .ids import slugify_task_segment
9
+ from .jsonl import read_jsonl
10
+
11
+
12
+ def predict_next_run_seq(project_root: Path, task_group: str, task_id: str,
13
+ task_type: str, *,
14
+ home: Optional[Path] = None,
15
+ project_id: Optional[str] = None) -> int:
16
+ """타깃 프로젝트와 (선택적) 중앙 인덱스를 합쳐 다음 run_seq 를 계산.
17
+
18
+ 스캔 소스:
19
+ 1. reports/ 의 final-report-<task_type>-<seq:03d>.md (완료된 run)
20
+ 2. manifests/ 의 run-manifest-<task_type>-<seq:03d>.json (시작했지만 종료 전)
21
+ 3. home/active.jsonl 의 같은 (project_id, group, task_id, task_type) row
22
+ (다른 okstra-ctl 프로세스가 이미 예약한 in-flight run) — home/project_id 가 주어진 경우만
23
+
24
+ 이 셋의 max + 1 이 안전한 다음 seq.
25
+ """
26
+ task_type_segment = slugify_task_segment(task_type)
27
+ base = (project_root / ".project-docs" / "okstra" / "tasks"
28
+ / slugify_task_segment(task_group) / slugify_task_segment(task_id)
29
+ / "runs" / task_type_segment)
30
+ max_seq = 0
31
+ rep_pat = _re.compile(rf"^final-report-{_re.escape(task_type_segment)}-(\d+)\.md$")
32
+ man_pat = _re.compile(rf"^run-manifest-{_re.escape(task_type_segment)}-(\d+)\.json$")
33
+ for sub, pat in (("reports", rep_pat), ("manifests", man_pat)):
34
+ d = base / sub
35
+ if not d.is_dir():
36
+ continue
37
+ for child in d.iterdir():
38
+ m = pat.match(child.name)
39
+ if m:
40
+ n = int(m.group(1))
41
+ if n > max_seq:
42
+ max_seq = n
43
+ if home is not None and project_id is not None:
44
+ for src in ("active.jsonl", "recent.jsonl"):
45
+ for r in read_jsonl(home / src):
46
+ if (r.get("projectId") == project_id
47
+ and r.get("taskGroup") == task_group
48
+ and r.get("taskId") == task_id
49
+ and r.get("taskType") == task_type):
50
+ s = int(r.get("runSeq", 0))
51
+ if s > max_seq:
52
+ max_seq = s
53
+ return max_seq + 1
@@ -0,0 +1,33 @@
1
+ """Claude session helpers.
2
+
3
+ bash session.sh 의 python 구현. claude session id 생성, resume command
4
+ 파일 작성.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+
13
+ def generate_claude_session_id() -> str:
14
+ """UUIDv4 문자열."""
15
+ return str(uuid.uuid4())
16
+
17
+
18
+ def write_claude_resume_command_file(
19
+ *, resume_command_path: Path, project_root: Path, claude_session_id: str,
20
+ ) -> None:
21
+ """`bash claude-resume-*.sh` 를 실행하면 task 의 claude 세션을 resume
22
+ 하도록 shell 스크립트를 작성하고 chmod +x.
23
+ """
24
+ resume_command_path = Path(resume_command_path)
25
+ resume_command_path.parent.mkdir(parents=True, exist_ok=True)
26
+ body = (
27
+ "#!/usr/bin/env bash\n"
28
+ "# Generated by okstra. Resume the prepared Claude session for this run.\n"
29
+ f'cd "{project_root}"\n'
30
+ f'exec claude --resume "{claude_session_id}"\n'
31
+ )
32
+ resume_command_path.write_text(body, encoding="utf-8")
33
+ os.chmod(resume_command_path, 0o755)
@@ -0,0 +1,27 @@
1
+ """tmux 명령 빌더."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+
7
+ def _shell_quote(s: str) -> str:
8
+ """POSIX shell 안전 인용. shlex.quote 가 모든 메타문자(&, ;, |, *, ?, …) 를 처리한다."""
9
+ import shlex as _shlex
10
+ return _shlex.quote(s)
11
+
12
+
13
+ def build_tmux_command(*, session_name: str, cwd: str, run_seq: int,
14
+ argv: list, okstra_script: str,
15
+ extra_env: Optional[dict] = None) -> list:
16
+ """tmux new-session 명령을 list 형태로 반환.
17
+ extra_env 의 키는 inner 명령 앞에 KEY=VAL 형태로 prepend 된다 — tmux 세션은
18
+ 호출 셸의 환경을 자동 상속하지 않으므로 OKSTRA_HOME 등은 명시 전달해야 한다.
19
+ """
20
+ env_prefix = f"OKSTRA_RUN_SEQ_OVERRIDE={run_seq}"
21
+ if extra_env:
22
+ for k, v in extra_env.items():
23
+ env_prefix += f" {k}={_shell_quote(str(v))}"
24
+ # 스크립트 경로에 공백/메타문자가 있어도 안전하도록 동일 방식으로 인용.
25
+ inner = (f"{env_prefix} {_shell_quote(okstra_script)} "
26
+ + " ".join(_shell_quote(a) for a in argv))
27
+ return ["tmux", "new-session", "-d", "-s", session_name, "-c", cwd, inner]
@@ -0,0 +1,64 @@
1
+ """Worker roster resolution.
2
+
3
+ Profile 파일에서 권장 worker 목록을 뽑고(`resolve_profile_workers`), 사용자
4
+ 오버라이드와 합쳐 정규화한다(`normalize_workers`). bash workers.sh 의
5
+ 동등한 python 구현.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ ALLOWED_WORKERS = ["claude", "codex", "gemini", "report-writer"]
12
+ PROFILE_BULLET_HEADERS = {"- Workers:", "- Reviewers:", "- Analysers:"}
13
+
14
+
15
+ class WorkersError(Exception):
16
+ """invalid worker selection — surface to user."""
17
+
18
+
19
+ def resolve_profile_workers(profile_path: Path) -> list[str]:
20
+ """`prompts/profiles/<task-type>.md` 본문의 `- Workers:` 섹션 아래
21
+ sub-bullet 들을 worker id 리스트로 돌려준다.
22
+ profile 파일이 없거나 섹션이 없으면 빈 리스트.
23
+ """
24
+ if not Path(profile_path).is_file():
25
+ return []
26
+ capturing = False
27
+ out: list[str] = []
28
+ for line in Path(profile_path).read_text(encoding="utf-8").splitlines():
29
+ stripped = line.strip()
30
+ if stripped in PROFILE_BULLET_HEADERS:
31
+ capturing = True
32
+ continue
33
+ if not capturing:
34
+ continue
35
+ if line.startswith("- "):
36
+ break
37
+ if line.startswith(" - "):
38
+ out.append(line[4:].strip())
39
+ continue
40
+ if stripped:
41
+ break
42
+ return out
43
+
44
+
45
+ def normalize_workers(value: str) -> list[str]:
46
+ """CSV 입력을 정규화한다.
47
+
48
+ - 공백 strip, 소문자화, 중복 제거(첫 출현 우선).
49
+ - 빈 입력이면 `ALLOWED_WORKERS` 전체를 default 로 사용.
50
+ - 허용 외 worker 가 포함되면 `WorkersError`.
51
+ """
52
+ items = [v.strip().lower() for v in (value or "").split(",") if v.strip()]
53
+ source = items or ALLOWED_WORKERS
54
+ unknown = [v for v in source if v not in ALLOWED_WORKERS]
55
+ if unknown:
56
+ raise WorkersError(f"unknown workers: {','.join(unknown)}")
57
+ seen: set[str] = set()
58
+ out: list[str] = []
59
+ for v in source:
60
+ if v in seen:
61
+ continue
62
+ seen.add(v)
63
+ out.append(v)
64
+ return out
@@ -0,0 +1,182 @@
1
+ """Phase / workflow state computation.
2
+
3
+ bash `compute_workflow_render_context` + `default_next_phase_for_task_type` 의
4
+ python 구현. (task-type, current task/run status, render-only flag) 만 받아서
5
+ WORKFLOW_* 필드 dict 를 돌려준다.
6
+
7
+ 또한 task-type 별 PHASE_ALLOWED_OUTPUTS / PHASE_FORBIDDEN_ACTIONS 본문 매핑을
8
+ 모듈 상수로 보유.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ PHASE_SEQUENCE = [
13
+ "requirements-discovery",
14
+ "error-analysis",
15
+ "implementation-planning",
16
+ "implementation",
17
+ "final-verification",
18
+ ]
19
+
20
+ DEFAULT_NEXT_PHASE = {
21
+ "requirements-discovery": "pending-routing-decision",
22
+ "error-analysis": "implementation-planning",
23
+ "implementation-planning": "implementation",
24
+ "implementation": "final-verification",
25
+ "final-verification": "done-or-follow-up",
26
+ }
27
+
28
+ # Phase 별 allowed outputs / forbidden actions. bash heredoc 원문 그대로 옮긴 값.
29
+ # 들여쓰기와 백틱은 prompt template 에 그대로 박혀 lead 가 읽는다.
30
+ PHASE_RULES: dict[str, dict[str, str]] = {
31
+ "requirements-discovery": {
32
+ "allowed": (
33
+ " - work-category classification (bugfix / feature / refactor / ops / improvement)\n"
34
+ " - routing decision toward the next safe lifecycle phase\n"
35
+ " - missing-input list and clarification requests\n"
36
+ " - approval / confirmation checkpoints recorded for the next phase"
37
+ ),
38
+ "forbidden": (
39
+ " - source code edits of any kind\n"
40
+ " - implementation planning or detailed design beyond what is required to choose the next phase\n"
41
+ " - executing builds, migrations, deployments, or any state-mutating command\n"
42
+ " - starting `error-analysis`, `implementation-planning`, or `implementation` inside this run (each must be a separate run, and `implementation` additionally requires an approved `implementation-planning` deliverable)"
43
+ ),
44
+ },
45
+ "error-analysis": {
46
+ "allowed": (
47
+ " - symptom and trigger evidence\n"
48
+ " - root-cause hypotheses with supporting citations\n"
49
+ " - reproduction gaps and missing observability\n"
50
+ " - validation paths to confirm or refute hypotheses"
51
+ ),
52
+ "forbidden": (
53
+ " - source code edits, refactors, or fix attempts\n"
54
+ " - implementation design or planning artifacts\n"
55
+ " - executing builds, migrations, deployments, or any state-mutating command\n"
56
+ " - starting `implementation-planning` or `implementation` inside this run (each must be a separate run, and `implementation` additionally requires an approved `implementation-planning` deliverable)"
57
+ ),
58
+ },
59
+ "implementation-planning": {
60
+ "allowed": (
61
+ " - pre-planning context exploration notes (files/interfaces inspected, recent commits scanned, ambiguities flagged)\n"
62
+ " - at least two implementation option candidates, each with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
63
+ " - trade-off matrix across options (complexity, risk, reversibility, test cost, rollout cost) and recommended option with rationale tied to isolation / single-responsibility / YAGNI principles\n"
64
+ " - bite-sized stepwise execution order for the recommended option (each step ~2-5 min, exact file paths and commands, TDD ordering when applicable, no placeholders)\n"
65
+ " - dependency / migration risk assessment, validation checklist (pre / mid / post with exact commands), rollback strategy with revert path and trigger signal\n"
66
+ " - `Open Questions` block listing every unresolved ambiguity\n"
67
+ " - explicit `User Approval Request` block awaiting human approval\n"
68
+ " - self-review confirmation (spec coverage, placeholder scan, internal consistency, ambiguity, scope)"
69
+ ),
70
+ "forbidden": (
71
+ " - source code edits of any kind (Edit/Write on project source files is forbidden)\n"
72
+ " - file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`); in particular, do not write to `docs/superpowers/specs/` or `docs/superpowers/plans/`\n"
73
+ " - executing builds, migrations, deployments, or any state-mutating command\n"
74
+ ' - starting `implementation` inside this run (must be a separate run authorised by an approved deliverable from this phase), even if the user says "다음 단계 진행해"\n'
75
+ " - dispatching parallel sub-agents beyond the required worker roster (okstra owns worker fan-out)\n"
76
+ ' - leaving placeholders such as TBD / TODO / "handle edge cases" / "similar to Option N" in the report\n'
77
+ " - delegating the self-review pass to a generic subagent — `Claude lead` must run it"
78
+ ),
79
+ },
80
+ "implementation": {
81
+ "allowed": (
82
+ " - approved-plan reference and quoted user-approval evidence\n"
83
+ " - commit list with SHA, message, and the plan step each commit satisfies\n"
84
+ " - `git diff --stat <base>..HEAD` summary plus per-file one-line change summary\n"
85
+ " - `Out-of-plan edits` block listing every file touched outside the approved plan with rationale (empty block preferred)\n"
86
+ ' - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
87
+ " - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
88
+ " - per-verifier sections (`Gemini verifier`, `Codex verifier`, `Claude verifier`) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
89
+ " - rollback verification (revert SHA reachable, feature flag toggle works, migration down step valid; dry-run preferred)\n"
90
+ " - routing recommendation for `final-verification` (ready / needs new error-analysis or planning loop)"
91
+ ),
92
+ "forbidden": (
93
+ " - any Edit/Write or state-mutating Bash before the pre-implementation gate passes (gate requires --approved-plan pointing to a final-report.md with a recorded user approval marker)\n"
94
+ " - `git push` of any kind, `npm publish` / `cargo publish` / `pip publish`, `gh release`, `docker push`\n"
95
+ " - real database migrations, schema changes against shared environments, or writes to non-local datastores\n"
96
+ " - production credentials, deploy commands, infra mutation (`terraform apply`, `kubectl apply` against non-local cluster, etc.)\n"
97
+ " - external API write calls (POST/PUT/PATCH/DELETE) to third-party services other than localhost test fixtures\n"
98
+ " - source edits or Bash mutations performed by any verifier role (`Gemini verifier`, `Codex verifier`, `Claude verifier` are read-only — recommend, do not apply)\n"
99
+ " - dispatching parallel sub-agents beyond the required worker roster\n"
100
+ " - silent scope expansion: every file edited outside the approved plan list MUST appear in the `Out-of-plan edits` block with rationale\n"
101
+ ' - leaving placeholders such as TBD / TODO / "implement later" / "handle edge cases" in committed code\n'
102
+ ' - declaring overall task acceptance — that is `final-verification` ownership; this phase reports only "ready for final-verification" or "needs new planning loop"\n'
103
+ " - delegating the self-review pass to a generic subagent — `Claude lead` must run it"
104
+ ),
105
+ },
106
+ "final-verification": {
107
+ "allowed": (
108
+ " - acceptance verdict with requirement coverage assessment\n"
109
+ " - residual risk and regression notes\n"
110
+ " - recommended follow-up routing (`error-analysis` / `implementation-planning`) for any defects detected"
111
+ ),
112
+ "forbidden": (
113
+ " - source code edits, follow-up bug fixes, or scope expansion\n"
114
+ " - state-mutating commands; only read-only execution of pre-existing test or validation commands is permitted\n"
115
+ " - starting any follow-up phase inside this run; record findings and end the run"
116
+ ),
117
+ },
118
+ }
119
+
120
+ PHASE_RULES_UNKNOWN = {
121
+ "allowed": " - outputs defined by the active task type",
122
+ "forbidden": (
123
+ " - any action that belongs to a different lifecycle phase\n"
124
+ " - source code edits or state-mutating commands unless this task type explicitly authorises them"
125
+ ),
126
+ }
127
+
128
+
129
+ def default_next_phase_for(task_type: str) -> str:
130
+ return DEFAULT_NEXT_PHASE.get(task_type, "unknown")
131
+
132
+
133
+ def compute_workflow_state(
134
+ *,
135
+ task_type: str,
136
+ current_run_status: str,
137
+ current_task_status: str,
138
+ render_only: bool,
139
+ ) -> dict:
140
+ """WORKFLOW_* + PHASE_* 값을 dict 로 돌려준다."""
141
+ if current_run_status == "in-progress":
142
+ phase_state = "in-progress"
143
+ elif current_run_status == "prepared":
144
+ phase_state = "prepared"
145
+ elif current_run_status in ("error", "timeout", "contract-violated"):
146
+ phase_state = "blocked"
147
+ elif current_run_status == "completed":
148
+ phase_state = "completed"
149
+ else:
150
+ if current_task_status in (
151
+ "instruction-set-generated",
152
+ "ready-for-claude",
153
+ "claude-session-started",
154
+ ):
155
+ phase_state = "prepared"
156
+ else:
157
+ phase_state = "not-started"
158
+
159
+ routing_status = "pending" if task_type == "requirements-discovery" else "not-applicable"
160
+
161
+ if render_only:
162
+ checkpoint_label = "instruction-set-prepared"
163
+ elif current_run_status == "in-progress":
164
+ checkpoint_label = "interactive-session-handoff"
165
+ else:
166
+ checkpoint_label = "run-prepared"
167
+
168
+ rules = PHASE_RULES.get(task_type, PHASE_RULES_UNKNOWN)
169
+ last_completed = task_type if current_run_status == "completed" else ""
170
+
171
+ return {
172
+ "WORKFLOW_WORK_CATEGORY": "unknown",
173
+ "WORKFLOW_CURRENT_PHASE": task_type,
174
+ "WORKFLOW_CURRENT_PHASE_STATE": phase_state,
175
+ "WORKFLOW_NEXT_RECOMMENDED_PHASE": default_next_phase_for(task_type),
176
+ "WORKFLOW_LAST_COMPLETED_PHASE": last_completed,
177
+ "WORKFLOW_AWAITING_APPROVAL": "false",
178
+ "WORKFLOW_ROUTING_STATUS": routing_status,
179
+ "WORKFLOW_LAST_SAFE_CHECKPOINT_LABEL": checkpoint_label,
180
+ "PHASE_ALLOWED_OUTPUTS": rules["allowed"],
181
+ "PHASE_FORBIDDEN_ACTIONS": rules["forbidden"],
182
+ }
@@ -0,0 +1,41 @@
1
+ """Project root self-registration model.
2
+
3
+ `<PROJECT_ROOT>/.project-docs/okstra/project.json` 을 권위 소스로 삼아
4
+ PROJECT_ROOT 를 해석하고, 실행 시점에 upsert 한다. 과거 모델
5
+ (`examples/projects/<id>.conf.sh`) 는 폐기되었다.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from .resolver import (
10
+ ResolverError,
11
+ project_json_path,
12
+ resolve_project_root,
13
+ upsert_project_json,
14
+ )
15
+ from .state import (
16
+ StateError,
17
+ find_task_root,
18
+ list_project_tasks,
19
+ parse_task_key,
20
+ read_latest_task,
21
+ read_task_catalog,
22
+ read_task_manifest,
23
+ resolve_task_identity,
24
+ slugify,
25
+ )
26
+
27
+ __all__ = [
28
+ "ResolverError",
29
+ "StateError",
30
+ "project_json_path",
31
+ "resolve_project_root",
32
+ "upsert_project_json",
33
+ "find_task_root",
34
+ "list_project_tasks",
35
+ "parse_task_key",
36
+ "read_latest_task",
37
+ "read_task_catalog",
38
+ "read_task_manifest",
39
+ "resolve_task_identity",
40
+ "slugify",
41
+ ]