okstra 0.135.0 → 0.137.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/docs/architecture/storage-model.md +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +8 -3
- package/package.json +2 -3
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/interactive.sh +24 -67
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
- package/runtime/bin/okstra-antigravity-exec.sh +10 -8
- package/runtime/bin/okstra-claude-exec.sh +10 -8
- package/runtime/bin/okstra-codex-exec.sh +10 -8
- package/runtime/bin/okstra-spawn-followups.py +4 -9
- package/runtime/prompts/lead/plan-body-verification.md +2 -2
- package/runtime/python/okstra_ctl/backfill.py +2 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +56 -331
- package/runtime/python/okstra_ctl/consumers.py +7 -5
- package/runtime/python/okstra_ctl/context_cost.py +6 -5
- package/runtime/python/okstra_ctl/dispatch_core.py +54 -175
- package/runtime/python/okstra_ctl/dispatch_state.py +231 -0
- package/runtime/python/okstra_ctl/error_log_core.py +3 -1
- package/runtime/python/okstra_ctl/error_report.py +2 -7
- package/runtime/python/okstra_ctl/handoff.py +2 -1
- package/runtime/python/okstra_ctl/implementation_outcome.py +17 -27
- package/runtime/python/okstra_ctl/implementation_stage.py +7 -2
- package/runtime/python/okstra_ctl/path_hints.py +5 -3
- package/runtime/python/okstra_ctl/paths.py +278 -4
- package/runtime/python/okstra_ctl/plan_run_root.py +4 -4
- package/runtime/python/okstra_ctl/recap.py +5 -12
- package/runtime/python/okstra_ctl/render.py +9 -15
- package/runtime/python/okstra_ctl/report_finalize.py +8 -4
- package/runtime/python/okstra_ctl/report_language.py +83 -0
- package/runtime/python/okstra_ctl/run.py +222 -239
- package/runtime/python/okstra_ctl/set_work_status.py +2 -1
- package/runtime/python/okstra_ctl/stage_targets.py +98 -55
- package/runtime/python/okstra_ctl/time_report.py +4 -9
- package/runtime/python/okstra_ctl/wizard.py +50 -50
- package/runtime/python/okstra_ctl/work_categories.py +2 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +82 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +0 -13
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +7 -0
- package/runtime/python/okstra_project/__init__.py +4 -0
- package/runtime/python/okstra_project/dirs.py +7 -0
- package/runtime/python/okstra_project/state.py +78 -8
- package/runtime/validators/validate-run.py +23 -52
- package/src/commands/inspect/stage-map.mjs +12 -19
|
@@ -30,6 +30,7 @@ from .dirs import (
|
|
|
30
30
|
DISCOVERY_RELATIVE,
|
|
31
31
|
LATEST_TASK_RELATIVE,
|
|
32
32
|
TASK_CATALOG_RELATIVE,
|
|
33
|
+
TASK_MANIFEST_FILENAME,
|
|
33
34
|
TASKS_RELATIVE,
|
|
34
35
|
)
|
|
35
36
|
|
|
@@ -82,7 +83,24 @@ def read_latest_task(project_root: Path) -> Optional[dict]:
|
|
|
82
83
|
|
|
83
84
|
def read_task_manifest(task_root: Path) -> Optional[dict]:
|
|
84
85
|
"""<task-root>/task-manifest.json 을 dict 로. 파일 없으면 None."""
|
|
85
|
-
return _load_json(Path(task_root) /
|
|
86
|
+
return _load_json(Path(task_root) / TASK_MANIFEST_FILENAME)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def read_task_key(task_root: Path) -> str:
|
|
90
|
+
"""manifest 의 taskKey. 파일이 없거나 깨졌으면 "".
|
|
91
|
+
|
|
92
|
+
리포트 렌더러(recap·time-report·error-report)는 taskKey 를 헤더 라벨로만 쓰므로
|
|
93
|
+
manifest 가 없어도 실패하지 않아야 한다 — 세 모듈이 각자 갖고 있던 관용 리더를
|
|
94
|
+
여기로 모은다.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
manifest = read_task_manifest(Path(task_root))
|
|
98
|
+
except (StateError, OSError):
|
|
99
|
+
return ""
|
|
100
|
+
if not isinstance(manifest, dict):
|
|
101
|
+
return ""
|
|
102
|
+
value = manifest.get("taskKey")
|
|
103
|
+
return value if isinstance(value, str) else ""
|
|
86
104
|
|
|
87
105
|
|
|
88
106
|
def _reconcile_task_root_best_effort(project_root: Path, task_root: Path) -> None:
|
|
@@ -94,6 +112,31 @@ def _reconcile_task_root_best_effort(project_root: Path, task_root: Path) -> Non
|
|
|
94
112
|
return
|
|
95
113
|
|
|
96
114
|
|
|
115
|
+
_DERIVED_STATUS_STRING_FIELDS = (
|
|
116
|
+
"workStatus",
|
|
117
|
+
"currentStatus",
|
|
118
|
+
"latestRunStatus",
|
|
119
|
+
"latestReportPath",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def derived_task_status(manifest: dict) -> dict:
|
|
124
|
+
"""Task status a caller acts on, projected off the raw manifest.
|
|
125
|
+
|
|
126
|
+
Both the task list rows and the read-side snapshot project through here so
|
|
127
|
+
the two views cannot disagree about whether a task is finished.
|
|
128
|
+
"""
|
|
129
|
+
out: dict = {}
|
|
130
|
+
for field in _DERIVED_STATUS_STRING_FIELDS:
|
|
131
|
+
value = manifest.get(field)
|
|
132
|
+
if isinstance(value, str):
|
|
133
|
+
out[field] = value
|
|
134
|
+
phase_outcome = manifest.get("phaseOutcome")
|
|
135
|
+
if isinstance(phase_outcome, dict):
|
|
136
|
+
out["phaseOutcome"] = phase_outcome
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
97
140
|
def _entry_with_manifest_state(entry: dict, task_root: Path) -> dict:
|
|
98
141
|
out = {**entry}
|
|
99
142
|
manifest = read_task_manifest(task_root) or {}
|
|
@@ -107,13 +150,7 @@ def _entry_with_manifest_state(entry: dict, task_root: Path) -> dict:
|
|
|
107
150
|
value = workflow.get(source)
|
|
108
151
|
if isinstance(value, str):
|
|
109
152
|
out[target] = value
|
|
110
|
-
|
|
111
|
-
value = manifest.get(field)
|
|
112
|
-
if isinstance(value, str):
|
|
113
|
-
out[field] = value
|
|
114
|
-
phase_outcome = manifest.get("phaseOutcome")
|
|
115
|
-
if isinstance(phase_outcome, dict):
|
|
116
|
-
out["phaseOutcome"] = phase_outcome
|
|
153
|
+
out.update(derived_task_status(manifest))
|
|
117
154
|
return out
|
|
118
155
|
|
|
119
156
|
|
|
@@ -329,8 +366,41 @@ def task_read_side_snapshot(project_root: Path, task_key: str) -> dict:
|
|
|
329
366
|
"routingStatus": workflow.get("routingStatus"),
|
|
330
367
|
"phaseStates": workflow.get("phaseStates"),
|
|
331
368
|
},
|
|
369
|
+
"status": derived_task_status(manifest),
|
|
332
370
|
"resultContract": manifest.get("resultContract"),
|
|
333
371
|
"artifacts": manifest.get("artifacts"),
|
|
334
372
|
"modelAssignments": manifest.get("modelAssignments"),
|
|
335
373
|
"latestRunPath": manifest.get("latestRunPath"),
|
|
336
374
|
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def stage_map_read_side_snapshot(project_root: Path, task_key: str) -> dict:
|
|
378
|
+
"""stage-map inspect 용 read-side view: Stage Map + done 처리된 stage 번호.
|
|
379
|
+
|
|
380
|
+
Stage Map 파싱과 planning run-root 위치를 caller 에게 노출하지 않는다 —
|
|
381
|
+
Node inspect 가 okstra_ctl 의 private 심볼을 import 하거나 `runs/` 경로를
|
|
382
|
+
직접 조립하지 않도록 하는 어댑터다.
|
|
383
|
+
"""
|
|
384
|
+
from okstra_ctl.consumers import read_stage_consumer_state
|
|
385
|
+
from okstra_ctl.implementation_outcome import load_stage_map
|
|
386
|
+
from okstra_ctl.paths import RunRef
|
|
387
|
+
|
|
388
|
+
identity = resolve_task_identity(project_root, task_key)
|
|
389
|
+
task_root = Path(identity["taskRoot"])
|
|
390
|
+
stages = load_stage_map(task_root, identity["manifest"])
|
|
391
|
+
plan_run_root = RunRef.from_task_root(
|
|
392
|
+
task_root, "implementation-planning"
|
|
393
|
+
).run_dir
|
|
394
|
+
done: list[int] = []
|
|
395
|
+
if plan_run_root.is_dir():
|
|
396
|
+
done = sorted(
|
|
397
|
+
read_stage_consumer_state(
|
|
398
|
+
plan_run_root, recover_from_carry=True
|
|
399
|
+
).done_stages
|
|
400
|
+
)
|
|
401
|
+
return {
|
|
402
|
+
"taskKey": identity["taskKey"],
|
|
403
|
+
"taskRoot": identity["taskRoot"],
|
|
404
|
+
"stages": stages,
|
|
405
|
+
"doneStages": done,
|
|
406
|
+
}
|
|
@@ -43,6 +43,8 @@ from okstra_ctl.conformance import ( # noqa: E402
|
|
|
43
43
|
qa_result_from_dict,
|
|
44
44
|
validate_conformance_manifest,
|
|
45
45
|
)
|
|
46
|
+
from okstra_ctl.paths import RunRef # noqa: E402
|
|
47
|
+
from okstra_ctl.workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE # noqa: E402
|
|
46
48
|
from okstra_ctl.md_table import ( # noqa: E402
|
|
47
49
|
is_separator_row as _is_markdown_separator,
|
|
48
50
|
split_pipe_row as _split_pipe_row,
|
|
@@ -157,19 +159,7 @@ def write_json(path: Path, payload: dict) -> None:
|
|
|
157
159
|
|
|
158
160
|
|
|
159
161
|
def default_next_phase(task_type: str) -> str:
|
|
160
|
-
|
|
161
|
-
"requirements-discovery": "pending-routing-decision",
|
|
162
|
-
"improvement-discovery": "pending-routing-decision",
|
|
163
|
-
"error-analysis": "implementation-planning",
|
|
164
|
-
"implementation-planning": "implementation",
|
|
165
|
-
"implementation": "final-verification",
|
|
166
|
-
# final-verification 의 다음 phase 는 verdict 에 따라 갈리므로
|
|
167
|
-
# 정적 매핑은 `pending-release-handoff` 로 두고, 실제 진입은
|
|
168
|
-
# release-handoff profile 의 entry gate (`accepted` 확인) 에서 강제한다.
|
|
169
|
-
"final-verification": "pending-release-handoff",
|
|
170
|
-
"release-handoff": "done-or-follow-up",
|
|
171
|
-
}
|
|
172
|
-
return mapping.get(task_type, "unknown")
|
|
162
|
+
return DEFAULT_NEXT_PHASE.get(task_type, "unknown")
|
|
173
163
|
|
|
174
164
|
|
|
175
165
|
def advance_next_phase(
|
|
@@ -208,14 +198,7 @@ def update_workflow_metadata(
|
|
|
208
198
|
)
|
|
209
199
|
phase_sequence = workflow.get("phaseSequence", [])
|
|
210
200
|
if not isinstance(phase_sequence, list) or not phase_sequence:
|
|
211
|
-
phase_sequence =
|
|
212
|
-
"requirements-discovery",
|
|
213
|
-
"error-analysis",
|
|
214
|
-
"implementation-planning",
|
|
215
|
-
"implementation",
|
|
216
|
-
"final-verification",
|
|
217
|
-
"release-handoff",
|
|
218
|
-
]
|
|
201
|
+
phase_sequence = list(PHASE_SEQUENCE)
|
|
219
202
|
|
|
220
203
|
phase_states = workflow.get("phaseStates", {})
|
|
221
204
|
if not isinstance(phase_states, dict):
|
|
@@ -1143,17 +1126,15 @@ def _scope_manifest_entries(manifest: dict, stage_name: str | None) -> dict:
|
|
|
1143
1126
|
|
|
1144
1127
|
|
|
1145
1128
|
def _task_root_from_run_dir(run_dir: Path) -> Path:
|
|
1146
|
-
"""run_dir 에서
|
|
1129
|
+
"""run_dir 에서 task_root 를 복원한다.
|
|
1147
1130
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
→ 같은 task_root. `runs` 를 위로 탐색하므로 stage-<N> 레벨이 추가돼도
|
|
1151
|
-
안전하다. `runs` 가 조상에 없으면 기존 동작(두 단계 위)로 폴백한다.
|
|
1131
|
+
레이아웃 해석은 `RunRef.from_run_dir` 이 소유한다. run 디렉터리가 아닌
|
|
1132
|
+
입력에는 기존 동작(두 단계 위)으로 폴백해 검증기가 죽지 않게 한다.
|
|
1152
1133
|
"""
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1134
|
+
try:
|
|
1135
|
+
return RunRef.from_run_dir(run_dir).task_root
|
|
1136
|
+
except ValueError:
|
|
1137
|
+
return run_dir.parent.parent
|
|
1157
1138
|
|
|
1158
1139
|
|
|
1159
1140
|
def _read_run_inputs_payload(
|
|
@@ -3129,19 +3110,15 @@ _PASSING_VERDICT_TOKENS = frozenset({"accepted", "conditional-accept"})
|
|
|
3129
3110
|
def _consumers_rows(report_path: Path) -> list[dict] | None:
|
|
3130
3111
|
"""`runs/implementation-planning/consumers.jsonl` rows for this task.
|
|
3131
3112
|
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
a missing artifact into a false accusation.
|
|
3113
|
+
``None`` when the report path is unreadable as a run reference or the file
|
|
3114
|
+
is absent, so the caller does not turn a missing artifact into a false
|
|
3115
|
+
accusation.
|
|
3136
3116
|
"""
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
runs_dir = parent
|
|
3141
|
-
break
|
|
3142
|
-
if runs_dir is None:
|
|
3117
|
+
try:
|
|
3118
|
+
ref = RunRef.from_report_path(report_path)
|
|
3119
|
+
except ValueError:
|
|
3143
3120
|
return None
|
|
3144
|
-
path =
|
|
3121
|
+
path = ref.sibling("implementation-planning").run_dir / "consumers.jsonl"
|
|
3145
3122
|
if not path.is_file():
|
|
3146
3123
|
return None
|
|
3147
3124
|
rows = []
|
|
@@ -3497,17 +3474,11 @@ def _validate_stage_carry_sidecar_exists(
|
|
|
3497
3474
|
stage = evidence.get("stageNumber")
|
|
3498
3475
|
if not isinstance(stage, int):
|
|
3499
3476
|
return
|
|
3500
|
-
# Carry sidecars are stage-SHARED:
|
|
3501
|
-
# `
|
|
3502
|
-
#
|
|
3503
|
-
#
|
|
3504
|
-
|
|
3505
|
-
# `runs/implementation/stage-<N>/reports/final-report-...md`, so drop the
|
|
3506
|
-
# stage-<N>/ segment to reach the flat carry dir that `consumers` reads —
|
|
3507
|
-
# resolving it under the stage run dir forced the lead to write the file twice.
|
|
3508
|
-
run_dir = report_path.parent.parent
|
|
3509
|
-
carry_root = run_dir.parent if _stage_isolated_name(run_dir) else run_dir
|
|
3510
|
-
carry_path = carry_root / "carry" / f"stage-{stage}.json"
|
|
3477
|
+
# Carry sidecars are stage-SHARED: the next stage's carry-in and
|
|
3478
|
+
# `consumers.backfill_done_from_carry` glob them without knowing the
|
|
3479
|
+
# producing run's layout. `RunRef.carry()` owns that flat-vs-staged rule;
|
|
3480
|
+
# resolving it under the stage run dir forced the lead to write it twice.
|
|
3481
|
+
carry_path = RunRef.from_report_path(report_path).carry(stage)
|
|
3511
3482
|
if not carry_path.exists():
|
|
3512
3483
|
failures.append(
|
|
3513
3484
|
f"implementation run declares stage-{stage} sidecar evidence but "
|
|
@@ -40,9 +40,9 @@ function parseArgs(args) {
|
|
|
40
40
|
const SCRIPT = `
|
|
41
41
|
import json, sys
|
|
42
42
|
from pathlib import Path
|
|
43
|
-
from okstra_project import
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
from okstra_project import (
|
|
44
|
+
resolve_project_root, stage_map_read_side_snapshot, ResolverError, StateError,
|
|
45
|
+
)
|
|
46
46
|
|
|
47
47
|
explicit, cwd, task_key = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
48
48
|
|
|
@@ -52,24 +52,17 @@ except ResolverError as e:
|
|
|
52
52
|
print(json.dumps({"ok": False, "stage": "resolve", "reason": str(e)}))
|
|
53
53
|
sys.exit(2)
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
try:
|
|
56
|
+
snapshot = stage_map_read_side_snapshot(Path(pr), task_key)
|
|
57
|
+
except StateError as e:
|
|
58
|
+
print(json.dumps({
|
|
59
|
+
"ok": False,
|
|
60
|
+
"stage": getattr(e, "stage", None) or "stage_map",
|
|
61
|
+
"reason": str(e),
|
|
62
|
+
}))
|
|
58
63
|
sys.exit(1)
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
stages = _load_stage_map(task_root, manifest)
|
|
62
|
-
|
|
63
|
-
plan_run_root = task_root / "runs" / "implementation-planning"
|
|
64
|
-
done = sorted(read_stage_consumer_state(plan_run_root, recover_from_carry=True).done_stages) if plan_run_root.is_dir() else []
|
|
65
|
-
|
|
66
|
-
print(json.dumps({
|
|
67
|
-
"ok": True,
|
|
68
|
-
"taskKey": task_key,
|
|
69
|
-
"taskRoot": str(task_root),
|
|
70
|
-
"stages": stages,
|
|
71
|
-
"doneStages": done,
|
|
72
|
-
}, ensure_ascii=False, indent=2))
|
|
65
|
+
print(json.dumps({"ok": True, **snapshot}, ensure_ascii=False, indent=2))
|
|
73
66
|
`;
|
|
74
67
|
|
|
75
68
|
export async function run(args) {
|