okstra 0.136.0 → 0.138.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/architecture.md +3 -2
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +32 -5
- package/package.json +2 -3
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
- package/runtime/bin/okstra-spawn-followups.py +4 -9
- package/runtime/prompts/lead/adapters/claude-code.md +1 -0
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/plan-body-verification.md +2 -2
- package/runtime/prompts/lead/report-writer.md +11 -10
- package/runtime/prompts/lead/team-contract.md +4 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +199 -681
- package/runtime/python/okstra_ctl/consumers.py +7 -5
- package/runtime/python/okstra_ctl/context_cost.py +4 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +146 -287
- package/runtime/python/okstra_ctl/dispatch_state.py +231 -0
- package/runtime/python/okstra_ctl/error_report.py +2 -7
- package/runtime/python/okstra_ctl/implementation_outcome.py +12 -23
- package/runtime/python/okstra_ctl/implementation_stage.py +7 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +1053 -0
- package/runtime/python/okstra_ctl/path_hints.py +3 -3
- package/runtime/python/okstra_ctl/paths.py +17 -2
- package/runtime/python/okstra_ctl/recap.py +5 -12
- package/runtime/python/okstra_ctl/render.py +18 -19
- 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 +262 -328
- package/runtime/python/okstra_ctl/set_work_status.py +2 -1
- package/runtime/python/okstra_ctl/stage_targets.py +330 -101
- package/runtime/python/okstra_ctl/time_report.py +4 -9
- package/runtime/python/okstra_ctl/wizard.py +47 -49
- 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 +73 -13
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +7 -0
- package/runtime/python/okstra_ctl/worktree.py +37 -29
- 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/lib/fixtures.sh +2 -0
- package/runtime/validators/validate-run.py +3 -21
- 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
|
+
}
|
|
@@ -137,6 +137,7 @@ prompt_path.write_text(
|
|
|
137
137
|
f"**Errors sidecar path:** {project_root}/run/worker-results/"
|
|
138
138
|
f"{target_worker_id}-errors.json",
|
|
139
139
|
f"Assigned worker prompt history path: {prompt_relative}",
|
|
140
|
+
"**Prompt Delivery Mode:** eager-include",
|
|
140
141
|
f"- Primary analysis packet: `{analysis_packet_path}`",
|
|
141
142
|
"",
|
|
142
143
|
"# Initial Analysis Prompt",
|
|
@@ -230,6 +231,7 @@ for worker in team_state.get("workers", []):
|
|
|
230
231
|
f"**Errors sidecar path:** {project_root}/run/worker-results/"
|
|
231
232
|
f"{worker_id}-errors.json",
|
|
232
233
|
f"Assigned worker prompt history path: {prompt_relative}",
|
|
234
|
+
"**Prompt Delivery Mode:** eager-include",
|
|
233
235
|
]
|
|
234
236
|
if worker_id != "report-writer":
|
|
235
237
|
prompt_lines.append(
|
|
@@ -44,6 +44,7 @@ from okstra_ctl.conformance import ( # noqa: E402
|
|
|
44
44
|
validate_conformance_manifest,
|
|
45
45
|
)
|
|
46
46
|
from okstra_ctl.paths import RunRef # noqa: E402
|
|
47
|
+
from okstra_ctl.workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE # noqa: E402
|
|
47
48
|
from okstra_ctl.md_table import ( # noqa: E402
|
|
48
49
|
is_separator_row as _is_markdown_separator,
|
|
49
50
|
split_pipe_row as _split_pipe_row,
|
|
@@ -158,19 +159,7 @@ def write_json(path: Path, payload: dict) -> None:
|
|
|
158
159
|
|
|
159
160
|
|
|
160
161
|
def default_next_phase(task_type: str) -> str:
|
|
161
|
-
|
|
162
|
-
"requirements-discovery": "pending-routing-decision",
|
|
163
|
-
"improvement-discovery": "pending-routing-decision",
|
|
164
|
-
"error-analysis": "implementation-planning",
|
|
165
|
-
"implementation-planning": "implementation",
|
|
166
|
-
"implementation": "final-verification",
|
|
167
|
-
# final-verification 의 다음 phase 는 verdict 에 따라 갈리므로
|
|
168
|
-
# 정적 매핑은 `pending-release-handoff` 로 두고, 실제 진입은
|
|
169
|
-
# release-handoff profile 의 entry gate (`accepted` 확인) 에서 강제한다.
|
|
170
|
-
"final-verification": "pending-release-handoff",
|
|
171
|
-
"release-handoff": "done-or-follow-up",
|
|
172
|
-
}
|
|
173
|
-
return mapping.get(task_type, "unknown")
|
|
162
|
+
return DEFAULT_NEXT_PHASE.get(task_type, "unknown")
|
|
174
163
|
|
|
175
164
|
|
|
176
165
|
def advance_next_phase(
|
|
@@ -209,14 +198,7 @@ def update_workflow_metadata(
|
|
|
209
198
|
)
|
|
210
199
|
phase_sequence = workflow.get("phaseSequence", [])
|
|
211
200
|
if not isinstance(phase_sequence, list) or not phase_sequence:
|
|
212
|
-
phase_sequence =
|
|
213
|
-
"requirements-discovery",
|
|
214
|
-
"error-analysis",
|
|
215
|
-
"implementation-planning",
|
|
216
|
-
"implementation",
|
|
217
|
-
"final-verification",
|
|
218
|
-
"release-handoff",
|
|
219
|
-
]
|
|
201
|
+
phase_sequence = list(PHASE_SEQUENCE)
|
|
220
202
|
|
|
221
203
|
phase_states = workflow.get("phaseStates", {})
|
|
222
204
|
if not isinstance(phase_states, dict):
|
|
@@ -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) {
|