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.
Files changed (50) hide show
  1. package/docs/architecture/storage-model.md +1 -1
  2. package/docs/architecture.md +3 -2
  3. package/docs/contributor-change-matrix.md +1 -1
  4. package/docs/project-structure-overview.md +32 -5
  5. package/package.json +2 -3
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/antigravity-worker.md +1 -1
  8. package/runtime/agents/workers/codex-worker.md +1 -1
  9. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
  10. package/runtime/bin/okstra-spawn-followups.py +4 -9
  11. package/runtime/prompts/lead/adapters/claude-code.md +1 -0
  12. package/runtime/prompts/lead/adapters/codex.md +1 -0
  13. package/runtime/prompts/lead/adapters/external.md +1 -0
  14. package/runtime/prompts/lead/plan-body-verification.md +2 -2
  15. package/runtime/prompts/lead/report-writer.md +11 -10
  16. package/runtime/prompts/lead/team-contract.md +4 -2
  17. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  18. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  19. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  20. package/runtime/python/okstra_ctl/codex_dispatch.py +199 -681
  21. package/runtime/python/okstra_ctl/consumers.py +7 -5
  22. package/runtime/python/okstra_ctl/context_cost.py +4 -4
  23. package/runtime/python/okstra_ctl/dispatch_core.py +146 -287
  24. package/runtime/python/okstra_ctl/dispatch_state.py +231 -0
  25. package/runtime/python/okstra_ctl/error_report.py +2 -7
  26. package/runtime/python/okstra_ctl/implementation_outcome.py +12 -23
  27. package/runtime/python/okstra_ctl/implementation_stage.py +7 -2
  28. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +1053 -0
  29. package/runtime/python/okstra_ctl/path_hints.py +3 -3
  30. package/runtime/python/okstra_ctl/paths.py +17 -2
  31. package/runtime/python/okstra_ctl/recap.py +5 -12
  32. package/runtime/python/okstra_ctl/render.py +18 -19
  33. package/runtime/python/okstra_ctl/report_finalize.py +8 -4
  34. package/runtime/python/okstra_ctl/report_language.py +83 -0
  35. package/runtime/python/okstra_ctl/run.py +262 -328
  36. package/runtime/python/okstra_ctl/set_work_status.py +2 -1
  37. package/runtime/python/okstra_ctl/stage_targets.py +330 -101
  38. package/runtime/python/okstra_ctl/time_report.py +4 -9
  39. package/runtime/python/okstra_ctl/wizard.py +47 -49
  40. package/runtime/python/okstra_ctl/work_categories.py +2 -2
  41. package/runtime/python/okstra_ctl/worker_prompt_body.py +82 -0
  42. package/runtime/python/okstra_ctl/worker_prompt_contract.py +73 -13
  43. package/runtime/python/okstra_ctl/worker_prompt_headers.py +7 -0
  44. package/runtime/python/okstra_ctl/worktree.py +37 -29
  45. package/runtime/python/okstra_project/__init__.py +4 -0
  46. package/runtime/python/okstra_project/dirs.py +7 -0
  47. package/runtime/python/okstra_project/state.py +78 -8
  48. package/runtime/validators/lib/fixtures.sh +2 -0
  49. package/runtime/validators/validate-run.py +3 -21
  50. 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) / "task-manifest.json")
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
- for field in ("currentStatus", "latestRunStatus", "latestReportPath"):
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
- mapping = {
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 resolve_project_root, find_task_root, read_task_manifest, ResolverError
44
- from okstra_ctl.implementation_outcome import _load_stage_map
45
- from okstra_ctl.consumers import read_stage_consumer_state
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
- task_root = find_task_root(Path(pr), task_key)
56
- if task_root is None:
57
- print(json.dumps({"ok": False, "stage": "task_root", "reason": f"task root not found for {task_key}"}))
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
- manifest = read_task_manifest(task_root) or {}
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) {