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.
- package/README.md +36 -0
- package/bin/okstra +62 -0
- package/package.json +30 -0
- package/runtime/.gitkeep +0 -0
- package/runtime/BUILD.json +5 -0
- package/runtime/agents/SKILL.md +243 -0
- package/runtime/agents/TODO.md +168 -0
- package/runtime/agents/workers/claude-worker.md +106 -0
- package/runtime/agents/workers/codex-worker.md +179 -0
- package/runtime/agents/workers/gemini-worker.md +179 -0
- package/runtime/agents/workers/report-writer-worker.md +116 -0
- package/runtime/bin/okstra-central.sh +152 -0
- package/runtime/bin/okstra-codex-exec.sh +53 -0
- package/runtime/bin/okstra-error-log.py +295 -0
- package/runtime/bin/okstra-gemini-exec.sh +55 -0
- package/runtime/bin/okstra-token-usage.py +46 -0
- package/runtime/bin/okstra.sh +162 -0
- package/runtime/prompts/launch.template.md +52 -0
- package/runtime/prompts/profiles/error-analysis.md +43 -0
- package/runtime/prompts/profiles/final-verification.md +37 -0
- package/runtime/prompts/profiles/implementation-planning.md +85 -0
- package/runtime/prompts/profiles/implementation.md +71 -0
- package/runtime/prompts/profiles/requirements-discovery.md +43 -0
- package/runtime/python/lib/okstra/cli.sh +227 -0
- package/runtime/python/lib/okstra/globals.sh +157 -0
- package/runtime/python/lib/okstra/interactive.sh +411 -0
- package/runtime/python/lib/okstra/project-resolver.sh +57 -0
- package/runtime/python/lib/okstra/usage.sh +98 -0
- package/runtime/python/lib/okstra-ctl/cmd-batch.sh +59 -0
- package/runtime/python/lib/okstra-ctl/cmd-list.sh +35 -0
- package/runtime/python/lib/okstra-ctl/cmd-open.sh +36 -0
- package/runtime/python/lib/okstra-ctl/cmd-projects.sh +26 -0
- package/runtime/python/lib/okstra-ctl/cmd-reconcile.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-reindex.sh +38 -0
- package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +326 -0
- package/runtime/python/lib/okstra-ctl/cmd-show.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-tail.sh +76 -0
- package/runtime/python/lib/okstra-ctl/main.sh +41 -0
- package/runtime/python/lib/okstra-ctl/prepare.sh +29 -0
- package/runtime/python/lib/okstra-ctl/usage.sh +23 -0
- package/runtime/python/okstra_ctl/__init__.py +125 -0
- package/runtime/python/okstra_ctl/backfill.py +253 -0
- package/runtime/python/okstra_ctl/batch.py +62 -0
- package/runtime/python/okstra_ctl/ids.py +84 -0
- package/runtime/python/okstra_ctl/index.py +216 -0
- package/runtime/python/okstra_ctl/invocation.py +49 -0
- package/runtime/python/okstra_ctl/jsonl.py +84 -0
- package/runtime/python/okstra_ctl/listing.py +156 -0
- package/runtime/python/okstra_ctl/locks.py +42 -0
- package/runtime/python/okstra_ctl/material.py +62 -0
- package/runtime/python/okstra_ctl/models.py +63 -0
- package/runtime/python/okstra_ctl/path_resolve.py +40 -0
- package/runtime/python/okstra_ctl/paths.py +251 -0
- package/runtime/python/okstra_ctl/project_meta.py +51 -0
- package/runtime/python/okstra_ctl/reconcile.py +166 -0
- package/runtime/python/okstra_ctl/render.py +1065 -0
- package/runtime/python/okstra_ctl/resolver.py +54 -0
- package/runtime/python/okstra_ctl/run.py +674 -0
- package/runtime/python/okstra_ctl/run_context.py +166 -0
- package/runtime/python/okstra_ctl/seeding.py +97 -0
- package/runtime/python/okstra_ctl/sequence.py +53 -0
- package/runtime/python/okstra_ctl/session.py +33 -0
- package/runtime/python/okstra_ctl/tmux.py +27 -0
- package/runtime/python/okstra_ctl/workers.py +64 -0
- package/runtime/python/okstra_ctl/workflow.py +182 -0
- package/runtime/python/okstra_project/__init__.py +41 -0
- package/runtime/python/okstra_project/resolver.py +126 -0
- package/runtime/python/okstra_project/state.py +170 -0
- package/runtime/python/okstra_token_usage/__init__.py +26 -0
- package/runtime/python/okstra_token_usage/blocks.py +62 -0
- package/runtime/python/okstra_token_usage/claude.py +97 -0
- package/runtime/python/okstra_token_usage/cli.py +84 -0
- package/runtime/python/okstra_token_usage/codex.py +80 -0
- package/runtime/python/okstra_token_usage/collect.py +161 -0
- package/runtime/python/okstra_token_usage/gemini.py +77 -0
- package/runtime/python/okstra_token_usage/jsonl_io.py +18 -0
- package/runtime/python/okstra_token_usage/paths.py +22 -0
- package/runtime/python/okstra_token_usage/pricing.py +71 -0
- package/runtime/python/okstra_token_usage/report.py +64 -0
- package/runtime/templates/prd/brief.template.md +273 -0
- package/runtime/templates/project-docs/task-index.template.md +65 -0
- package/runtime/templates/reports/error-analysis-input.template.md +80 -0
- package/runtime/templates/reports/final-report.template.md +167 -0
- package/runtime/templates/reports/final-verification-input.template.md +67 -0
- package/runtime/templates/reports/implementation-input.template.md +81 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +93 -0
- package/runtime/templates/reports/quick-input.template.md +64 -0
- package/runtime/templates/reports/schedule.template.md +168 -0
- package/runtime/templates/reports/settings.template.json +101 -0
- package/runtime/templates/reports/task-brief.template.md +165 -0
- package/runtime/validators/lib/common.sh +44 -0
- package/runtime/validators/lib/fixtures.sh +322 -0
- package/runtime/validators/lib/paths.sh +44 -0
- package/runtime/validators/lib/runners.sh +140 -0
- package/runtime/validators/lib/summary.sh +15 -0
- package/runtime/validators/lib/validate-assets.sh +44 -0
- package/runtime/validators/lib/validate-prompt-metadata.sh +267 -0
- package/runtime/validators/lib/validate-tasks.sh +335 -0
- package/runtime/validators/validate-run.py +568 -0
- package/runtime/validators/validate-schedule.py +665 -0
- package/runtime/validators/validate-workflow.sh +190 -0
- package/src/doctor.mjs +127 -0
- package/src/install.mjs +355 -0
- package/src/paths.mjs +132 -0
- package/src/uninstall.mjs +122 -0
- package/src/version.mjs +20 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""User-supplied file path resolution.
|
|
2
|
+
|
|
3
|
+
bash `resolve_brief_path`, `resolve_clarification_response_path`,
|
|
4
|
+
`relative_to_project_root` 의 python 구현. 우선순위: 절대/cwd-기준 → project
|
|
5
|
+
root 기준 → None.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_user_file(raw: str, project_root: Path) -> Optional[Path]:
|
|
14
|
+
"""`raw` 가 (a) 절대경로 또는 cwd 기준으로 존재하면 그 절대경로
|
|
15
|
+
(b) project_root 기준으로 존재하면 그 경로 (c) 둘 다 실패면 None.
|
|
16
|
+
|
|
17
|
+
bash 구현과 동일 의미. cwd-우선이 의도된 동작이다 — 사용자가 IDE 에서
|
|
18
|
+
프로젝트 일부 디렉터리에 cd 한 채 상대경로를 줄 수 있어야 함.
|
|
19
|
+
"""
|
|
20
|
+
if not raw:
|
|
21
|
+
return None
|
|
22
|
+
p = Path(raw)
|
|
23
|
+
if p.is_file():
|
|
24
|
+
return p.resolve()
|
|
25
|
+
if not p.is_absolute():
|
|
26
|
+
in_root = Path(project_root) / raw
|
|
27
|
+
if in_root.is_file():
|
|
28
|
+
return in_root.resolve()
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def relative_to_project_root(path: Path, project_root: Path) -> str:
|
|
33
|
+
"""project_root 안의 경로면 그 기준 상대경로 문자열을, 바깥이면 입력값
|
|
34
|
+
그대로 돌려준다."""
|
|
35
|
+
project_root = Path(project_root).resolve()
|
|
36
|
+
path = Path(path)
|
|
37
|
+
try:
|
|
38
|
+
return str(path.resolve().relative_to(project_root))
|
|
39
|
+
except (ValueError, OSError):
|
|
40
|
+
return str(path)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Pure path computation for an okstra run.
|
|
2
|
+
|
|
3
|
+
Inputs: project root, identity (project-id/task-group/task-id), task-type,
|
|
4
|
+
workspace root, optional run-seq override.
|
|
5
|
+
|
|
6
|
+
Output: dict with every path value the bash render-context used to expose as
|
|
7
|
+
environment variables (TASK_ROOT, RUN_DIR, RUN_MANIFESTS_DIR, RUN_MANIFEST_FILE,
|
|
8
|
+
FINAL_REPORT_FILE, ...). 호출자가 이 dict 를 그대로 인자로 사용하거나
|
|
9
|
+
`write_run_context()` 로 디스크에 박는다.
|
|
10
|
+
|
|
11
|
+
이 모듈은 read-only 한 path 계산만 담당한다. 디렉터리 생성, 파일 쓰기,
|
|
12
|
+
seq advancement 같은 부수효과는 `run_context.py` 가 담당한다.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from okstra_project.state import slugify
|
|
22
|
+
|
|
23
|
+
OKSTRA_RELATIVE = Path(".project-docs/okstra")
|
|
24
|
+
TASKS_RELATIVE = OKSTRA_RELATIVE / "tasks"
|
|
25
|
+
DISCOVERY_RELATIVE = OKSTRA_RELATIVE / "discovery"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def next_run_seq(run_seq_dir: Path, task_type_segment: str) -> int:
|
|
29
|
+
"""run_seq_dir 안에서 `*-<task-type>-NNN.<ext>` 파일을 스캔해 다음 seq 번호를
|
|
30
|
+
돌려준다. 디렉터리 부재 시 1.
|
|
31
|
+
"""
|
|
32
|
+
pattern = re.compile(rf"-{re.escape(task_type_segment)}-(\d{{3}})\.[^.]+$")
|
|
33
|
+
max_seq = 0
|
|
34
|
+
if run_seq_dir.is_dir():
|
|
35
|
+
for entry in os.listdir(run_seq_dir):
|
|
36
|
+
full = run_seq_dir / entry
|
|
37
|
+
if not full.is_file():
|
|
38
|
+
continue
|
|
39
|
+
m = pattern.search(entry)
|
|
40
|
+
if m:
|
|
41
|
+
n = int(m.group(1))
|
|
42
|
+
if n > max_seq:
|
|
43
|
+
max_seq = n
|
|
44
|
+
return max_seq + 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _rel(project_root: Path, target: Path) -> str:
|
|
48
|
+
"""target 을 project_root 기준 상대경로 문자열로 변환. project_root 바깥이면
|
|
49
|
+
절대경로 그대로 돌려준다(workspace-root 같은 경우)."""
|
|
50
|
+
try:
|
|
51
|
+
return str(target.resolve().relative_to(project_root.resolve()))
|
|
52
|
+
except (ValueError, OSError):
|
|
53
|
+
return str(target)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compute_run_paths(
|
|
57
|
+
*,
|
|
58
|
+
project_root: Path,
|
|
59
|
+
workspace_root: Path,
|
|
60
|
+
project_id: str,
|
|
61
|
+
task_group: str,
|
|
62
|
+
task_id: str,
|
|
63
|
+
task_type: str,
|
|
64
|
+
run_seq_override: Optional[int] = None,
|
|
65
|
+
) -> dict:
|
|
66
|
+
"""주어진 identity 와 task-type 에 대해 모든 path/segment 값을 계산해
|
|
67
|
+
dict 로 돌려준다. 부수효과 없음.
|
|
68
|
+
|
|
69
|
+
run_seq_override: 정수가 주어지면 7개 seq 모두 그 값으로 강제. okstra-ctl
|
|
70
|
+
rerun 처럼 외부에서 seq 를 미리 reserve 한 경로용. 미지정 시 디스크 스캔
|
|
71
|
+
결과로 결정.
|
|
72
|
+
"""
|
|
73
|
+
project_root = Path(project_root)
|
|
74
|
+
workspace_root = Path(workspace_root)
|
|
75
|
+
|
|
76
|
+
task_group_segment = slugify(task_group)
|
|
77
|
+
task_id_segment = slugify(task_id)
|
|
78
|
+
task_type_segment = slugify(task_type)
|
|
79
|
+
|
|
80
|
+
if not task_group_segment or not task_id_segment:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"task-group and task-id must contain at least one alphanumeric character")
|
|
83
|
+
if not task_type_segment:
|
|
84
|
+
raise ValueError("task-type must contain at least one alphanumeric character")
|
|
85
|
+
|
|
86
|
+
okstra_root = project_root / OKSTRA_RELATIVE
|
|
87
|
+
tasks_root = project_root / TASKS_RELATIVE
|
|
88
|
+
discovery_dir = project_root / DISCOVERY_RELATIVE
|
|
89
|
+
|
|
90
|
+
task_root = tasks_root / task_group_segment / task_id_segment
|
|
91
|
+
task_manifest = task_root / "task-manifest.json"
|
|
92
|
+
task_index = task_root / "task-index.md"
|
|
93
|
+
instruction_set = task_root / "instruction-set"
|
|
94
|
+
runs_dir = task_root / "runs"
|
|
95
|
+
history_dir = task_root / "history"
|
|
96
|
+
timeline_file = history_dir / "timeline.json"
|
|
97
|
+
|
|
98
|
+
run_dir = runs_dir / task_type_segment
|
|
99
|
+
run_manifests = run_dir / "manifests"
|
|
100
|
+
run_state = run_dir / "state"
|
|
101
|
+
run_prompts = run_dir / "prompts"
|
|
102
|
+
run_reports = run_dir / "reports"
|
|
103
|
+
run_status = run_dir / "status"
|
|
104
|
+
run_sessions = run_dir / "sessions"
|
|
105
|
+
run_logs = run_dir / "logs"
|
|
106
|
+
worker_results = run_dir / "worker-results"
|
|
107
|
+
|
|
108
|
+
if run_seq_override is not None:
|
|
109
|
+
seq_int = int(run_seq_override)
|
|
110
|
+
seqs = {k: f"{seq_int:03d}" for k in (
|
|
111
|
+
"manifests", "prompts", "reports", "status", "state",
|
|
112
|
+
"sessions", "worker_results")}
|
|
113
|
+
else:
|
|
114
|
+
seqs = {
|
|
115
|
+
"manifests": f"{next_run_seq(run_manifests, task_type_segment):03d}",
|
|
116
|
+
"prompts": f"{next_run_seq(run_prompts, task_type_segment):03d}",
|
|
117
|
+
"reports": f"{next_run_seq(run_reports, task_type_segment):03d}",
|
|
118
|
+
"status": f"{next_run_seq(run_status, task_type_segment):03d}",
|
|
119
|
+
"state": f"{next_run_seq(run_state, task_type_segment):03d}",
|
|
120
|
+
"sessions": f"{next_run_seq(run_sessions, task_type_segment):03d}",
|
|
121
|
+
"worker_results": f"{next_run_seq(worker_results, task_type_segment):03d}",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
suffixes = {k: f"-{task_type_segment}-{v}" for k, v in seqs.items()}
|
|
125
|
+
run_file_suffix = suffixes["reports"]
|
|
126
|
+
|
|
127
|
+
run_manifest_file = run_manifests / f"run-manifest{suffixes['manifests']}.json"
|
|
128
|
+
run_prompt_snapshot = run_prompts / f"claude-execution-prompt{suffixes['prompts']}.md"
|
|
129
|
+
claude_worker_prompt = run_prompts / f"claude-worker-prompt{suffixes['prompts']}.md"
|
|
130
|
+
codex_worker_prompt = run_prompts / f"codex-worker-prompt{suffixes['prompts']}.md"
|
|
131
|
+
gemini_worker_prompt = run_prompts / f"gemini-worker-prompt{suffixes['prompts']}.md"
|
|
132
|
+
report_writer_worker_prompt = run_prompts / f"report-writer-worker-prompt{suffixes['prompts']}.md"
|
|
133
|
+
final_report = run_reports / f"final-report{suffixes['reports']}.md"
|
|
134
|
+
final_status = run_status / f"final{suffixes['status']}.status"
|
|
135
|
+
team_state = run_state / f"team-state{suffixes['state']}.json"
|
|
136
|
+
final_report_template = instruction_set / "final-report-template.md"
|
|
137
|
+
reference_expectations = instruction_set / "reference-expectations.md"
|
|
138
|
+
claude_resume_command = run_sessions / f"claude-resume{suffixes['sessions']}.sh"
|
|
139
|
+
latest_task_file = discovery_dir / "latest-task.json"
|
|
140
|
+
task_catalog_file = discovery_dir / "task-catalog.json"
|
|
141
|
+
claude_worker_result = worker_results / f"claude-worker{suffixes['worker_results']}.md"
|
|
142
|
+
codex_worker_result = worker_results / f"codex-worker{suffixes['worker_results']}.md"
|
|
143
|
+
gemini_worker_result = worker_results / f"gemini-worker{suffixes['worker_results']}.md"
|
|
144
|
+
report_writer_worker_result = worker_results / f"report-writer-worker{suffixes['worker_results']}.md"
|
|
145
|
+
|
|
146
|
+
run_validator_script = workspace_root / "validators" / "validate-run.py"
|
|
147
|
+
|
|
148
|
+
abs_paths = {
|
|
149
|
+
"PROJECT_ID": project_id,
|
|
150
|
+
"PROJECT_ROOT": str(project_root),
|
|
151
|
+
"WORKSPACE_ROOT": str(workspace_root),
|
|
152
|
+
"TASK_GROUP": task_group,
|
|
153
|
+
"TASK_ID": task_id,
|
|
154
|
+
"TASK_KEY": f"{project_id}:{task_group}:{task_id}",
|
|
155
|
+
"ANALYSIS_TYPE": task_type,
|
|
156
|
+
"TASK_GROUP_SEGMENT": task_group_segment,
|
|
157
|
+
"TASK_ID_SEGMENT": task_id_segment,
|
|
158
|
+
"TASK_TYPE_SEGMENT": task_type_segment,
|
|
159
|
+
"OKSTRA_ROOT": str(okstra_root),
|
|
160
|
+
"OKSTRA_TASKS_ROOT": str(tasks_root),
|
|
161
|
+
"OKSTRA_DISCOVERY_DIR": str(discovery_dir),
|
|
162
|
+
"TASK_ROOT": str(task_root),
|
|
163
|
+
"TASK_MANIFEST_FILE": str(task_manifest),
|
|
164
|
+
"TASK_INDEX_FILE": str(task_index),
|
|
165
|
+
"INSTRUCTION_SET_DIR": str(instruction_set),
|
|
166
|
+
"RUNS_DIR": str(runs_dir),
|
|
167
|
+
"HISTORY_DIR": str(history_dir),
|
|
168
|
+
"TIMELINE_FILE": str(timeline_file),
|
|
169
|
+
"RUN_DIR": str(run_dir),
|
|
170
|
+
"RUN_MANIFESTS_DIR": str(run_manifests),
|
|
171
|
+
"RUN_STATE_DIR": str(run_state),
|
|
172
|
+
"RUN_PROMPTS_DIR": str(run_prompts),
|
|
173
|
+
"RUN_REPORTS_DIR": str(run_reports),
|
|
174
|
+
"RUN_STATUS_DIR": str(run_status),
|
|
175
|
+
"RUN_SESSIONS_DIR": str(run_sessions),
|
|
176
|
+
"RUN_LOGS_DIR": str(run_logs),
|
|
177
|
+
"WORKER_RESULTS_DIR": str(worker_results),
|
|
178
|
+
"RUN_MANIFEST_FILE": str(run_manifest_file),
|
|
179
|
+
"RUN_PROMPT_SNAPSHOT_FILE": str(run_prompt_snapshot),
|
|
180
|
+
"CLAUDE_WORKER_PROMPT_FILE": str(claude_worker_prompt),
|
|
181
|
+
"CODEX_WORKER_PROMPT_FILE": str(codex_worker_prompt),
|
|
182
|
+
"GEMINI_WORKER_PROMPT_FILE": str(gemini_worker_prompt),
|
|
183
|
+
"REPORT_WRITER_WORKER_PROMPT_FILE": str(report_writer_worker_prompt),
|
|
184
|
+
"FINAL_REPORT_FILE": str(final_report),
|
|
185
|
+
"FINAL_STATUS_FILE": str(final_status),
|
|
186
|
+
"TEAM_STATE_FILE": str(team_state),
|
|
187
|
+
"FINAL_REPORT_TEMPLATE_FILE": str(final_report_template),
|
|
188
|
+
"REFERENCE_EXPECTATIONS_FILE": str(reference_expectations),
|
|
189
|
+
"CLAUDE_RESUME_COMMAND_FILE": str(claude_resume_command),
|
|
190
|
+
"OKSTRA_LATEST_TASK_FILE": str(latest_task_file),
|
|
191
|
+
"OKSTRA_TASK_CATALOG_FILE": str(task_catalog_file),
|
|
192
|
+
"CLAUDE_WORKER_RESULT_FILE": str(claude_worker_result),
|
|
193
|
+
"CODEX_WORKER_RESULT_FILE": str(codex_worker_result),
|
|
194
|
+
"GEMINI_WORKER_RESULT_FILE": str(gemini_worker_result),
|
|
195
|
+
"REPORT_WRITER_WORKER_RESULT_FILE": str(report_writer_worker_result),
|
|
196
|
+
"RUN_VALIDATOR_SCRIPT": str(run_validator_script),
|
|
197
|
+
"RUN_MANIFEST_FILENAME": run_manifest_file.name,
|
|
198
|
+
"RUN_PROMPT_SNAPSHOT_FILENAME": run_prompt_snapshot.name,
|
|
199
|
+
"FINAL_REPORT_FILENAME": final_report.name,
|
|
200
|
+
"FINAL_STATUS_FILENAME": final_status.name,
|
|
201
|
+
"CLAUDE_RESUME_COMMAND_FILENAME": claude_resume_command.name,
|
|
202
|
+
"RUN_FILE_SUFFIX": run_file_suffix,
|
|
203
|
+
"RUN_MANIFESTS_SEQ": seqs["manifests"],
|
|
204
|
+
"RUN_PROMPTS_SEQ": seqs["prompts"],
|
|
205
|
+
"RUN_REPORTS_SEQ": seqs["reports"],
|
|
206
|
+
"RUN_STATUS_SEQ": seqs["status"],
|
|
207
|
+
"RUN_STATE_SEQ": seqs["state"],
|
|
208
|
+
"RUN_SESSIONS_SEQ": seqs["sessions"],
|
|
209
|
+
"WORKER_RESULTS_SEQ": seqs["worker_results"],
|
|
210
|
+
"LATEST_RUN_PATH": str(run_dir),
|
|
211
|
+
}
|
|
212
|
+
rel_pairs = [
|
|
213
|
+
("OKSTRA_DISCOVERY_RELATIVE_PATH", discovery_dir),
|
|
214
|
+
("OKSTRA_LATEST_TASK_RELATIVE_PATH", latest_task_file),
|
|
215
|
+
("OKSTRA_TASK_CATALOG_RELATIVE_PATH", task_catalog_file),
|
|
216
|
+
("TASK_ROOT_RELATIVE_PATH", task_root),
|
|
217
|
+
("TASK_MANIFEST_RELATIVE_PATH", task_manifest),
|
|
218
|
+
("TASK_INDEX_RELATIVE_PATH", task_index),
|
|
219
|
+
("INSTRUCTION_SET_RELATIVE_PATH", instruction_set),
|
|
220
|
+
("RUNS_RELATIVE_PATH", runs_dir),
|
|
221
|
+
("HISTORY_RELATIVE_PATH", history_dir),
|
|
222
|
+
("TIMELINE_RELATIVE_PATH", timeline_file),
|
|
223
|
+
("RUN_DIR_RELATIVE_PATH", run_dir),
|
|
224
|
+
("RUN_MANIFESTS_RELATIVE_PATH", run_manifests),
|
|
225
|
+
("RUN_STATE_RELATIVE_PATH", run_state),
|
|
226
|
+
("RUN_PROMPTS_RELATIVE_PATH", run_prompts),
|
|
227
|
+
("RUN_REPORTS_RELATIVE_PATH", run_reports),
|
|
228
|
+
("RUN_STATUS_RELATIVE_PATH", run_status),
|
|
229
|
+
("RUN_SESSIONS_RELATIVE_PATH", run_sessions),
|
|
230
|
+
("RUN_MANIFEST_RELATIVE_PATH", run_manifest_file),
|
|
231
|
+
("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", run_prompt_snapshot),
|
|
232
|
+
("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", claude_worker_prompt),
|
|
233
|
+
("CODEX_WORKER_PROMPT_RELATIVE_PATH", codex_worker_prompt),
|
|
234
|
+
("GEMINI_WORKER_PROMPT_RELATIVE_PATH", gemini_worker_prompt),
|
|
235
|
+
("REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH", report_writer_worker_prompt),
|
|
236
|
+
("FINAL_REPORT_RELATIVE_PATH", final_report),
|
|
237
|
+
("FINAL_STATUS_RELATIVE_PATH", final_status),
|
|
238
|
+
("TEAM_STATE_RELATIVE_PATH", team_state),
|
|
239
|
+
("WORKER_RESULTS_RELATIVE_PATH", worker_results),
|
|
240
|
+
("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", final_report_template),
|
|
241
|
+
("REFERENCE_EXPECTATIONS_RELATIVE_PATH", reference_expectations),
|
|
242
|
+
("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", claude_resume_command),
|
|
243
|
+
("RUN_VALIDATOR_RELATIVE_PATH", run_validator_script),
|
|
244
|
+
("CLAUDE_WORKER_RESULT_RELATIVE_PATH", claude_worker_result),
|
|
245
|
+
("CODEX_WORKER_RESULT_RELATIVE_PATH", codex_worker_result),
|
|
246
|
+
("GEMINI_WORKER_RESULT_RELATIVE_PATH", gemini_worker_result),
|
|
247
|
+
("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", report_writer_worker_result),
|
|
248
|
+
("LATEST_RUN_RELATIVE_PATH", run_dir),
|
|
249
|
+
]
|
|
250
|
+
rel_paths = {key: _rel(project_root, target) for key, target in rel_pairs}
|
|
251
|
+
return {**abs_paths, **rel_paths}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""프로젝트 meta.json 업서트."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _project_meta_path(home: Path, project_id: str) -> Path:
|
|
11
|
+
return home / "projects" / project_id / "meta.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_project_meta(home: Path, project_id: str) -> Optional[dict]:
|
|
15
|
+
"""프로젝트 meta.json 을 읽는다. 없으면 None."""
|
|
16
|
+
p = _project_meta_path(home, project_id)
|
|
17
|
+
if not p.is_file():
|
|
18
|
+
return None
|
|
19
|
+
return json.loads(p.read_text())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upsert_project_meta(home: Path, project_id: str, *,
|
|
23
|
+
project_root: str, when: str,
|
|
24
|
+
started: bool = False, finished: bool = False) -> None:
|
|
25
|
+
"""프로젝트 meta.json 을 read-modify-write 로 갱신.
|
|
26
|
+
started=True 면 runCount+1, activeCount+1, lastRunAt 갱신, 최초이면 firstRunAt 도 설정.
|
|
27
|
+
finished=True 면 activeCount-1, lastRunAt 갱신.
|
|
28
|
+
"""
|
|
29
|
+
target = _project_meta_path(home, project_id)
|
|
30
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
if target.is_file():
|
|
32
|
+
meta = json.loads(target.read_text())
|
|
33
|
+
else:
|
|
34
|
+
meta = {
|
|
35
|
+
"projectId": project_id,
|
|
36
|
+
"projectRoot": project_root,
|
|
37
|
+
"firstRunAt": when,
|
|
38
|
+
"lastRunAt": when,
|
|
39
|
+
"runCount": 0,
|
|
40
|
+
"activeCount": 0,
|
|
41
|
+
}
|
|
42
|
+
meta["projectRoot"] = project_root
|
|
43
|
+
meta["lastRunAt"] = when
|
|
44
|
+
if started:
|
|
45
|
+
meta["runCount"] = int(meta.get("runCount", 0)) + 1
|
|
46
|
+
meta["activeCount"] = int(meta.get("activeCount", 0)) + 1
|
|
47
|
+
if finished:
|
|
48
|
+
meta["activeCount"] = max(0, int(meta.get("activeCount", 0)) - 1)
|
|
49
|
+
tmp = target.with_suffix(".json.tmp")
|
|
50
|
+
tmp.write_text(json.dumps(meta, indent=2) + "\n")
|
|
51
|
+
os.replace(tmp, target)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""reconciliation/normalization."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .jsonl import append_jsonl, read_jsonl, rotate_recent_if_needed
|
|
11
|
+
from .locks import central_lock
|
|
12
|
+
from .project_meta import upsert_project_meta
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_ABORT_AFTER_SECONDS = 12 * 3600 # 12시간 무진척이면 aborted 로 간주
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _now_iso() -> str:
|
|
19
|
+
return datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_iso(ts: str) -> Optional[datetime]:
|
|
23
|
+
try:
|
|
24
|
+
return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ")
|
|
25
|
+
except (ValueError, TypeError):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalize_central_status(manifest_status: str, validation_status: str) -> str:
|
|
30
|
+
"""Map okstra manifest/validation statuses to control-center statuses."""
|
|
31
|
+
failed_values = {
|
|
32
|
+
"failed",
|
|
33
|
+
"contract-violation",
|
|
34
|
+
"contract-violated",
|
|
35
|
+
"validation-failed",
|
|
36
|
+
}
|
|
37
|
+
manifest_status = str(manifest_status or "")
|
|
38
|
+
validation_status = str(validation_status or "")
|
|
39
|
+
if manifest_status in failed_values or validation_status in failed_values:
|
|
40
|
+
return "failed"
|
|
41
|
+
if manifest_status in {"aborted", "prepared", "running", "in-progress"}:
|
|
42
|
+
return manifest_status
|
|
43
|
+
if manifest_status in {"error", "errored"}:
|
|
44
|
+
return "failed"
|
|
45
|
+
return "completed"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_reconciled_report_status(validation_status: str) -> str:
|
|
49
|
+
"""Final report existence means terminal success unless validation failed."""
|
|
50
|
+
return normalize_central_status("completed", validation_status)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_run_manifest_validation(project_root: Path, run_dir_rel: str,
|
|
54
|
+
task_type: str, run_seq: int) -> str:
|
|
55
|
+
"""run-manifest 의 validation.status 를 읽는다. 못 읽으면 'not-run'.
|
|
56
|
+
완료/실패 여부를 정확히 반영하기 위함 — 단순히 final-report 존재만으로
|
|
57
|
+
'passed' 로 판정하면 contract-violation/failed 를 잘못 기록한다.
|
|
58
|
+
"""
|
|
59
|
+
if not run_dir_rel:
|
|
60
|
+
return "not-run"
|
|
61
|
+
suffix = f"-{task_type}-{run_seq:03d}"
|
|
62
|
+
manifest = project_root / run_dir_rel / "manifests" / f"run-manifest{suffix}.json"
|
|
63
|
+
if not manifest.is_file():
|
|
64
|
+
return "not-run"
|
|
65
|
+
try:
|
|
66
|
+
data = json.loads(manifest.read_text())
|
|
67
|
+
except (OSError, json.JSONDecodeError):
|
|
68
|
+
return "not-run"
|
|
69
|
+
validation = data.get("validation")
|
|
70
|
+
if isinstance(validation, dict):
|
|
71
|
+
return str(validation.get("status") or "not-run")
|
|
72
|
+
if isinstance(validation, str):
|
|
73
|
+
return validation
|
|
74
|
+
return "not-run"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def reconcile_active(home: Path, *,
|
|
78
|
+
abort_after_seconds: int = DEFAULT_ABORT_AFTER_SECONDS,
|
|
79
|
+
project: Optional[str] = None) -> dict:
|
|
80
|
+
"""active.jsonl 의 각 행을 검사해 종결 추론. 종결된 행은 recent 로 이동.
|
|
81
|
+
project 가 주어지면 해당 projectId 행만 추론 대상으로 삼고, 다른 프로젝트
|
|
82
|
+
행은 그대로 보존한다(스코프 reconcile).
|
|
83
|
+
중앙 락 안에서 read-modify-write 하므로 동시에 일어나는 record_start 와
|
|
84
|
+
같은 active 행 append 가 누락되지 않는다.
|
|
85
|
+
"""
|
|
86
|
+
with central_lock(home):
|
|
87
|
+
return _reconcile_active_locked(home,
|
|
88
|
+
abort_after_seconds=abort_after_seconds,
|
|
89
|
+
project=project)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _reconcile_active_locked(home: Path, *,
|
|
93
|
+
abort_after_seconds: int,
|
|
94
|
+
project: Optional[str] = None) -> dict:
|
|
95
|
+
active = home / "active.jsonl"
|
|
96
|
+
summary = {"completed": 0, "aborted": 0, "running": 0}
|
|
97
|
+
rows = read_jsonl(active)
|
|
98
|
+
if not rows:
|
|
99
|
+
return summary
|
|
100
|
+
survivors = []
|
|
101
|
+
promoted = []
|
|
102
|
+
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
|
103
|
+
for row in rows:
|
|
104
|
+
# project 필터: 스코프 외 행은 추론 없이 active 에 그대로 남긴다.
|
|
105
|
+
if project and row.get("projectId") != project:
|
|
106
|
+
survivors.append(row)
|
|
107
|
+
summary["running"] += 1
|
|
108
|
+
continue
|
|
109
|
+
project_root = Path(row.get("projectRoot", ""))
|
|
110
|
+
final_rel = row.get("finalReportRel", "")
|
|
111
|
+
final_abs = project_root / final_rel if final_rel else None
|
|
112
|
+
if final_abs and final_abs.is_file():
|
|
113
|
+
validation_status = _read_run_manifest_validation(
|
|
114
|
+
project_root, row.get("runDirRel", ""),
|
|
115
|
+
row.get("taskType", ""), int(row.get("runSeq", 0)))
|
|
116
|
+
# validate-okstra-run.py 가 아직 실행되지 않은 시점에는 final-report
|
|
117
|
+
# 만 존재하고 validation 은 'not-run' 으로 보고된다. 이때 promote 하면
|
|
118
|
+
# normalize_reconciled_report_status 가 'completed' 로 매핑하여 행이
|
|
119
|
+
# recent 로 옮겨가고, 이후 validator 가 failed/contract-violated 를
|
|
120
|
+
# 기록해도 reconcile 이 다시 보지 않아 영구히 잘못된 상태로 남는다.
|
|
121
|
+
# validation 이 terminal 신호('not-run' 이외) 일 때만 promote 하고,
|
|
122
|
+
# 그 외에는 active 에 유지해 후속 reconcile / abort timeout 에 맡긴다.
|
|
123
|
+
if validation_status and validation_status != "not-run":
|
|
124
|
+
row["status"] = normalize_reconciled_report_status(validation_status)
|
|
125
|
+
row["finishedAt"] = _now_iso()
|
|
126
|
+
row["validation"] = validation_status
|
|
127
|
+
promoted.append(row)
|
|
128
|
+
if row["status"] == "completed":
|
|
129
|
+
summary["completed"] += 1
|
|
130
|
+
else:
|
|
131
|
+
summary["completed"] += 0 # 실패도 종결로 분류하지만 카운터는 분리
|
|
132
|
+
continue
|
|
133
|
+
started = _parse_iso(row.get("startedAt", ""))
|
|
134
|
+
if started and (now - started).total_seconds() > abort_after_seconds:
|
|
135
|
+
row["status"] = "aborted"
|
|
136
|
+
row["finishedAt"] = _now_iso()
|
|
137
|
+
promoted.append(row)
|
|
138
|
+
summary["aborted"] += 1
|
|
139
|
+
continue
|
|
140
|
+
survivors.append(row)
|
|
141
|
+
summary["running"] += 1
|
|
142
|
+
if not promoted:
|
|
143
|
+
return summary
|
|
144
|
+
tmp = active.with_suffix(".jsonl.tmp")
|
|
145
|
+
with tmp.open("w") as f:
|
|
146
|
+
for row in survivors:
|
|
147
|
+
f.write(json.dumps(row, separators=(",", ":")) + "\n")
|
|
148
|
+
os.replace(tmp, active)
|
|
149
|
+
for row in promoted:
|
|
150
|
+
append_jsonl(home / "recent.jsonl", row)
|
|
151
|
+
proj_index = home / "projects" / row["projectId"] / "index.jsonl"
|
|
152
|
+
if proj_index.is_file():
|
|
153
|
+
existing = read_jsonl(proj_index)
|
|
154
|
+
for er in existing:
|
|
155
|
+
if er.get("runId") == row["runId"]:
|
|
156
|
+
er.update({k: row[k] for k in ("status", "finishedAt", "validation")})
|
|
157
|
+
tmp = proj_index.with_suffix(".jsonl.tmp")
|
|
158
|
+
with tmp.open("w") as f:
|
|
159
|
+
for er in existing:
|
|
160
|
+
f.write(json.dumps(er, separators=(",", ":")) + "\n")
|
|
161
|
+
os.replace(tmp, proj_index)
|
|
162
|
+
upsert_project_meta(home, row["projectId"],
|
|
163
|
+
project_root=row["projectRoot"],
|
|
164
|
+
when=row["finishedAt"], finished=True)
|
|
165
|
+
rotate_recent_if_needed(home)
|
|
166
|
+
return summary
|