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,1065 @@
1
+ """All run-artifact rendering, in one module.
2
+
3
+ 이 모듈은 기존에 `scripts/lib/okstra/{template,team-state,manifest,discovery,
4
+ directories}.sh` 의 python heredoc 으로 흩어져 있던 렌더 로직을 한 곳으로 모은다.
5
+ bash 측은 이제 heredoc 없이 `python3 -m okstra_ctl.render <subcommand> <args>`
6
+ 형태의 한 줄 호출로 같은 산출물을 만든다.
7
+
8
+ 설계 원칙:
9
+ - 각 함수는 (ctx dict, 추가 path 인자) 만 받아서 파일을 쓴다.
10
+ - 환경 변수에 의존하지 않는다 (ctx 가 권위).
11
+ - 단일 진입점 `main()` 이 subcommand 를 dispatcher 로 라우팅한다.
12
+
13
+ ctx dict 의 schema 는 `okstra_ctl.paths.compute_run_paths()` 의 반환값을
14
+ 기본으로, 호출자가 추가 키 (workflow state / model display / related tasks /
15
+ session id 등) 를 덧붙여 전달한다.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ # --------------------------------------------------------------------------- #
25
+ # helpers
26
+ # --------------------------------------------------------------------------- #
27
+
28
+
29
+ def _load_ctx(ctx_arg: str) -> dict:
30
+ """Accept either an inline JSON string or a path to a JSON file.
31
+
32
+ bash 는 보통 `build_render_context_json` 의 결과를 그대로 inline 으로 넘기고,
33
+ skill 은 디스크의 run-context 파일 경로를 넘긴다.
34
+ """
35
+ s = ctx_arg.lstrip()
36
+ if s.startswith("{"):
37
+ return json.loads(ctx_arg)
38
+ return json.loads(Path(ctx_arg).read_text(encoding="utf-8"))
39
+
40
+
41
+ def _write_text(path: Path, text: str) -> None:
42
+ path.parent.mkdir(parents=True, exist_ok=True)
43
+ path.write_text(text, encoding="utf-8")
44
+
45
+
46
+ def _write_json(path: Path, payload: dict) -> None:
47
+ _write_text(path, json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
48
+
49
+
50
+ def _resolve_workers(ctx: dict) -> list[str]:
51
+ return [w.strip() for w in ctx.get("SELECTED_REVIEWERS", "").split(",") if w.strip()]
52
+
53
+
54
+ def _worker_catalog(ctx: dict) -> dict:
55
+ return {
56
+ "claude": {
57
+ "workerId": "claude",
58
+ "role": "Claude worker",
59
+ "agent": "claude",
60
+ "agentLabel": "Claude Code",
61
+ "model": ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", ""),
62
+ "modelExecutionValue": ctx.get("CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""),
63
+ "resultPath": ctx.get("CLAUDE_WORKER_RESULT_RELATIVE_PATH", ""),
64
+ "promptPath": ctx.get("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", ""),
65
+ },
66
+ "codex": {
67
+ "workerId": "codex",
68
+ "role": "Codex worker",
69
+ "agent": "codex",
70
+ "agentLabel": "Codex",
71
+ "model": ctx.get("CODEX_WORKER_MODEL_DISPLAY", ""),
72
+ "modelExecutionValue": ctx.get("CODEX_WORKER_MODEL_EXECUTION_VALUE", ""),
73
+ "resultPath": ctx.get("CODEX_WORKER_RESULT_RELATIVE_PATH", ""),
74
+ "promptPath": ctx.get("CODEX_WORKER_PROMPT_RELATIVE_PATH", ""),
75
+ },
76
+ "gemini": {
77
+ "workerId": "gemini",
78
+ "role": "Gemini worker",
79
+ "agent": "gemini",
80
+ "agentLabel": "Gemini",
81
+ "model": ctx.get("GEMINI_WORKER_MODEL_DISPLAY", ""),
82
+ "modelExecutionValue": ctx.get("GEMINI_WORKER_MODEL_EXECUTION_VALUE", ""),
83
+ "resultPath": ctx.get("GEMINI_WORKER_RESULT_RELATIVE_PATH", ""),
84
+ "promptPath": ctx.get("GEMINI_WORKER_PROMPT_RELATIVE_PATH", ""),
85
+ },
86
+ "report-writer": {
87
+ "workerId": "report-writer",
88
+ "role": "Report writer worker",
89
+ "agent": "claude",
90
+ "agentLabel": "Claude Code",
91
+ "model": ctx.get("REPORT_WRITER_MODEL_DISPLAY", ""),
92
+ "modelExecutionValue": ctx.get("REPORT_WRITER_MODEL_EXECUTION_VALUE", ""),
93
+ "resultPath": ctx.get("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", ""),
94
+ "promptPath": ctx.get("REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH", ""),
95
+ },
96
+ }
97
+
98
+
99
+ # --------------------------------------------------------------------------- #
100
+ # team-state
101
+ # --------------------------------------------------------------------------- #
102
+
103
+
104
+ def render_team_state(team_state_path: str, ctx: dict) -> None:
105
+ selected = _resolve_workers(ctx)
106
+ catalog = _worker_catalog(ctx)
107
+ workers = []
108
+ for w in selected:
109
+ m = catalog[w]
110
+ workers.append({
111
+ "workerId": m["workerId"],
112
+ "role": m["role"],
113
+ "agent": m["agent"],
114
+ "model": m["model"],
115
+ "modelExecutionValue": m["modelExecutionValue"],
116
+ "status": "not-run",
117
+ "resultPath": m["resultPath"],
118
+ "promptPath": m["promptPath"],
119
+ "reason": "",
120
+ })
121
+ payload = {
122
+ "schemaVersion": "1.0",
123
+ "taskKey": ctx.get("TASK_KEY", ""),
124
+ "taskType": ctx.get("ANALYSIS_TYPE", ""),
125
+ "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
126
+ "workflowState": ctx.get("CURRENT_RUN_STATUS", ""),
127
+ "lead": {
128
+ "role": "Claude lead",
129
+ "agent": "claude",
130
+ "model": ctx.get("LEAD_MODEL_DISPLAY", ""),
131
+ "modelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
132
+ "status": ctx.get("CURRENT_RUN_STATUS", ""),
133
+ "sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
134
+ },
135
+ "workers": workers,
136
+ "validator": {
137
+ "scriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
138
+ "status": ctx.get("VALIDATION_STATUS", "not-run"),
139
+ "lastValidatedAt": ctx.get("VALIDATION_UPDATED_AT", ""),
140
+ "failures": json.loads(ctx.get("VALIDATION_FAILURES_JSON", "[]")),
141
+ },
142
+ "artifacts": {
143
+ "leadPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
144
+ "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
145
+ "finalReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
146
+ "finalStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
147
+ "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
148
+ },
149
+ }
150
+ _write_json(Path(team_state_path), payload)
151
+
152
+
153
+ # --------------------------------------------------------------------------- #
154
+ # reference expectations + discovery
155
+ # --------------------------------------------------------------------------- #
156
+
157
+
158
+ def render_reference_expectations(brief_path: str, output_path: str, ctx: dict) -> None:
159
+ section_map = {
160
+ "Configuration References and Expected Values": "config",
161
+ "Deployment Manifests and Expected Values": "deployment",
162
+ }
163
+ captured = {"config": [], "deployment": []}
164
+ current_section = None
165
+ for line in Path(brief_path).read_text(encoding="utf-8").splitlines():
166
+ if line.startswith("## "):
167
+ current_section = section_map.get(line[3:].strip())
168
+ continue
169
+ if current_section:
170
+ captured[current_section].append(line)
171
+
172
+ config_text = "\n".join(captured["config"]).strip()
173
+ deployment_text = "\n".join(captured["deployment"]).strip()
174
+ brief_relative = ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/task-brief.md"
175
+
176
+ parts = [
177
+ "# Task Reference Expectations",
178
+ "",
179
+ f"- Task Key: `{ctx.get('TASK_KEY', '')}`",
180
+ f"- Task Type: `{ctx.get('ANALYSIS_TYPE', '')}`",
181
+ f"- Source brief snapshot: `{brief_relative}`",
182
+ "",
183
+ "## Usage Rules",
184
+ "",
185
+ "- Treat this file as the canonical task-level reference for config files and deployment manifests that carry expected values for the current task.",
186
+ "- If a section below is empty, that means the task brief did not provide explicit expected-state guidance for that category.",
187
+ "- Missing expectations are missing information, not confirmed current state.",
188
+ "",
189
+ "## Configuration References and Expected Values",
190
+ "",
191
+ ]
192
+ parts.append(config_text or "- No explicit configuration-file expectations were provided in the task brief.")
193
+ parts.extend(["", "## Deployment Manifests and Expected Values", ""])
194
+ parts.append(deployment_text or "- No explicit deployment-manifest expectations were provided in the task brief.")
195
+ _write_text(Path(output_path), "\n".join(parts).rstrip() + "\n")
196
+
197
+
198
+ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
199
+ project_root = Path(ctx["PROJECT_ROOT"])
200
+ tasks_root = Path(ctx["OKSTRA_TASKS_ROOT"])
201
+
202
+ def s(payload, key):
203
+ if not isinstance(payload, dict):
204
+ return ""
205
+ v = payload.get(key, "")
206
+ return v if isinstance(v, str) else ""
207
+
208
+ def rel(p):
209
+ try:
210
+ return p.relative_to(project_root).as_posix()
211
+ except ValueError:
212
+ return str(p)
213
+
214
+ entries = []
215
+ for manifest_path in sorted(tasks_root.rglob("task-manifest.json")):
216
+ try:
217
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
218
+ except Exception:
219
+ continue
220
+ if not isinstance(manifest, dict):
221
+ continue
222
+ task_key = s(manifest, "taskKey").strip()
223
+ if not task_key:
224
+ continue
225
+ task_root = manifest_path.parent
226
+ timeline_relative = s(manifest, "historyTimelinePath").strip()
227
+ timeline_path = (project_root / timeline_relative) if timeline_relative else (task_root / "history" / "timeline.json")
228
+ latest_run = {}
229
+ if timeline_path.is_file():
230
+ try:
231
+ payload = json.loads(timeline_path.read_text(encoding="utf-8"))
232
+ except Exception:
233
+ payload = {}
234
+ runs = payload.get("runs", []) if isinstance(payload, dict) else []
235
+ if isinstance(runs, list):
236
+ for item in reversed(runs):
237
+ if isinstance(item, dict):
238
+ latest_run = item
239
+ break
240
+ workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
241
+ entries.append({
242
+ "taskKey": task_key,
243
+ "taskGroup": s(manifest, "taskGroup"),
244
+ "taskId": s(manifest, "taskId"),
245
+ "taskGroupPathSegment": s(manifest, "taskGroupPathSegment"),
246
+ "taskIdPathSegment": s(manifest, "taskIdPathSegment"),
247
+ "taskType": s(manifest, "taskType"),
248
+ "workCategory": s(manifest, "workCategory"),
249
+ "currentStatus": s(manifest, "currentStatus"),
250
+ "updatedAt": s(manifest, "updatedAt"),
251
+ "currentPhase": (workflow or {}).get("currentPhase", "") if isinstance(workflow, dict) else "",
252
+ "currentPhaseState": (workflow or {}).get("currentPhaseState", "") if isinstance(workflow, dict) else "",
253
+ "lastCompletedPhase": (workflow or {}).get("lastCompletedPhase", "") if isinstance(workflow, dict) else "",
254
+ "nextRecommendedPhase": (workflow or {}).get("nextRecommendedPhase", "") if isinstance(workflow, dict) else "",
255
+ "awaitingApproval": (workflow or {}).get("awaitingApproval", False) if isinstance(workflow, dict) else False,
256
+ "routingStatus": (workflow or {}).get("routingStatus", "") if isinstance(workflow, dict) else "",
257
+ "taskRootPath": s(manifest, "taskRootPath") or rel(task_root),
258
+ "taskManifestPath": s(manifest, "taskManifestPath") or rel(manifest_path),
259
+ "taskIndexPath": s(manifest, "taskIndexPath"),
260
+ "instructionSetPath": s(manifest, "instructionSetPath"),
261
+ "referenceExpectationsPath": s(manifest, "referenceExpectationsPath"),
262
+ "taskBriefPath": s(manifest, "taskBriefPath"),
263
+ "latestRunPath": s(manifest, "latestRunPath") or s(latest_run, "runDirectoryPath"),
264
+ "latestRunManifestPath": s(latest_run, "runManifestPath"),
265
+ "latestRunPromptsPath": s(manifest, "latestRunPromptsPath") or s(latest_run, "workerPromptDirectoryPath"),
266
+ "latestPromptSnapshotPath": s(latest_run, "promptSnapshotPath"),
267
+ "latestTeamStatePath": s(latest_run, "teamStatePath"),
268
+ "latestRunStatus": s(manifest, "latestRunStatus") or s(latest_run, "status"),
269
+ "latestReportPath": s(manifest, "latestReportPath") or s(latest_run, "reportPath"),
270
+ "latestResumeCommandPath": s(manifest, "latestResumeCommandPath") or s(latest_run, "resumeCommandPath"),
271
+ "historyTimelinePath": timeline_relative or rel(timeline_path),
272
+ })
273
+ entries.sort(key=lambda x: (x.get("updatedAt", ""), x.get("taskKey", "")), reverse=True)
274
+ payload = {
275
+ "schemaVersion": "1.0",
276
+ "projectId": ctx.get("PROJECT_ID", ""),
277
+ "updatedAt": ctx.get("RUN_TIMESTAMP_ISO", ""),
278
+ "latestTaskKey": ctx.get("TASK_KEY", ""),
279
+ "latestTaskDiscoveryPath": ctx.get("OKSTRA_LATEST_TASK_RELATIVE_PATH", ""),
280
+ "taskCount": len(entries),
281
+ "tasks": entries,
282
+ }
283
+ _write_json(Path(output_path), payload)
284
+
285
+
286
+ def render_latest_task_discovery(output_path: str, ctx: dict) -> None:
287
+ task_manifest_path = Path(ctx.get("TASK_MANIFEST_FILE", ""))
288
+ task_manifest = {}
289
+ if task_manifest_path.exists():
290
+ try:
291
+ task_manifest = json.loads(task_manifest_path.read_text(encoding="utf-8"))
292
+ except Exception:
293
+ task_manifest = {}
294
+ workflow = task_manifest.get("workflow") if isinstance(task_manifest.get("workflow"), dict) else {}
295
+ payload = {
296
+ "schemaVersion": "1.0",
297
+ "updatedAt": ctx.get("RUN_TIMESTAMP_ISO", ""),
298
+ "taskKey": ctx.get("TASK_KEY", ""),
299
+ "taskType": ctx.get("ANALYSIS_TYPE", ""),
300
+ "workCategory": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
301
+ "currentStatus": task_manifest.get("currentStatus", ctx.get("CURRENT_TASK_STATUS", "")),
302
+ "latestRunStatus": task_manifest.get("latestRunStatus", ctx.get("CURRENT_RUN_STATUS", "")),
303
+ "workflow": workflow,
304
+ "taskRootPath": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
305
+ "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
306
+ "taskIndexPath": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
307
+ "instructionSetPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", ""),
308
+ "taskCatalogPath": ctx.get("OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""),
309
+ "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
310
+ "latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
311
+ "latestRunManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
312
+ "latestRunPromptsPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
313
+ "latestPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
314
+ "latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
315
+ "latestReportPath": ctx.get("LATEST_REPORT_RELATIVE_PATH", ""),
316
+ "expectedReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
317
+ "expectedStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
318
+ "latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
319
+ "historyTimelinePath": ctx.get("TIMELINE_RELATIVE_PATH", ""),
320
+ }
321
+ _write_json(Path(output_path), payload)
322
+
323
+
324
+ # --------------------------------------------------------------------------- #
325
+ # directories migration
326
+ # --------------------------------------------------------------------------- #
327
+
328
+
329
+ def migrate_legacy_run_artifacts(ctx: dict) -> None:
330
+ import shutil
331
+
332
+ project_root = Path(ctx["PROJECT_ROOT"])
333
+ task_root = Path(ctx["TASK_ROOT"])
334
+ run_dir = Path(ctx["RUN_DIR"])
335
+ legacy_targets = [
336
+ ("run-manifest-", ".json", Path(ctx["RUN_MANIFESTS_DIR"])),
337
+ ("team-state-", ".json", Path(ctx["RUN_STATE_DIR"])),
338
+ ("claude-execution-prompt-", ".md", Path(ctx["RUN_PROMPTS_DIR"])),
339
+ ("final-report-", ".md", Path(ctx["RUN_REPORTS_DIR"])),
340
+ ("final-", ".status", Path(ctx["RUN_STATUS_DIR"])),
341
+ ("claude-resume-", ".sh", Path(ctx["RUN_SESSIONS_DIR"])),
342
+ ]
343
+ if not run_dir.is_dir():
344
+ return
345
+ path_rewrites = {}
346
+ for entry in run_dir.iterdir():
347
+ if not entry.is_file():
348
+ continue
349
+ for prefix, suffix, target_dir in legacy_targets:
350
+ if not entry.name.startswith(prefix) or not entry.name.endswith(suffix):
351
+ continue
352
+ target_dir.mkdir(parents=True, exist_ok=True)
353
+ destination = target_dir / entry.name
354
+ old_relative = str(entry.relative_to(project_root))
355
+ new_relative = str(destination.relative_to(project_root))
356
+ if destination.exists():
357
+ try:
358
+ if entry.read_bytes() == destination.read_bytes():
359
+ entry.unlink()
360
+ path_rewrites[old_relative] = new_relative
361
+ except Exception:
362
+ pass
363
+ break
364
+ shutil.move(str(entry), str(destination))
365
+ path_rewrites[old_relative] = new_relative
366
+ break
367
+ if not path_rewrites:
368
+ return
369
+ text_suffixes = {".json", ".md", ".txt", ".sh"}
370
+ for path in task_root.rglob("*"):
371
+ if not path.is_file() or path.suffix not in text_suffixes:
372
+ continue
373
+ try:
374
+ original = path.read_text(encoding="utf-8")
375
+ except Exception:
376
+ continue
377
+ updated = original
378
+ for old_rel, new_rel in path_rewrites.items():
379
+ updated = updated.replace(old_rel, new_rel)
380
+ if updated != original:
381
+ path.write_text(updated, encoding="utf-8")
382
+
383
+
384
+ # --------------------------------------------------------------------------- #
385
+ # task / run manifest + timeline + task-index
386
+ # --------------------------------------------------------------------------- #
387
+
388
+
389
+ def _required_worker_roles(ctx: dict, reviewers: list[str]) -> list[dict]:
390
+ catalog = _worker_catalog(ctx)
391
+ return [
392
+ {
393
+ "workerId": catalog[item]["workerId"],
394
+ "role": catalog[item]["role"],
395
+ "agent": catalog[item]["agent"],
396
+ "model": catalog[item]["model"],
397
+ "modelExecutionValue": catalog[item]["modelExecutionValue"],
398
+ "resultPath": catalog[item]["resultPath"],
399
+ "promptPath": catalog[item]["promptPath"],
400
+ "attemptRequired": True,
401
+ }
402
+ for item in reviewers
403
+ ]
404
+
405
+
406
+ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
407
+ path = Path(manifest_path)
408
+ existing = {}
409
+ if path.exists():
410
+ try:
411
+ existing = json.loads(path.read_text(encoding="utf-8"))
412
+ except Exception:
413
+ existing = {}
414
+ reviewers = _resolve_workers(ctx)
415
+ catalog = _worker_catalog(ctx)
416
+ phase_sequence = [
417
+ "requirements-discovery", "error-analysis", "implementation-planning",
418
+ "implementation", "final-verification",
419
+ ]
420
+ default_next_phase = {
421
+ "requirements-discovery": "pending-routing-decision",
422
+ "error-analysis": "implementation-planning",
423
+ "implementation-planning": "implementation",
424
+ "implementation": "final-verification",
425
+ "final-verification": "done-or-follow-up",
426
+ }
427
+ required_worker_roles = _required_worker_roles(ctx, reviewers)
428
+ worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
429
+ required_agent_status_entries = ["Claude lead"] + [catalog[item]["role"] for item in reviewers]
430
+ related_tasks = json.loads(ctx.get("RELATED_TASKS_JSON", "[]"))
431
+ current_report_relative = ctx.get("LATEST_REPORT_RELATIVE_PATH") or ctx.get("FINAL_REPORT_RELATIVE_PATH", "")
432
+ workflow = existing.get("workflow", {}) if isinstance(existing.get("workflow"), dict) else {}
433
+ phase_states = workflow.get("phaseStates", {}) if isinstance(workflow.get("phaseStates"), dict) else {}
434
+ current_phase = ctx.get("WORKFLOW_CURRENT_PHASE", ctx.get("ANALYSIS_TYPE", ""))
435
+ current_phase_state = ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "not-started")
436
+ for phase in phase_sequence:
437
+ phase_states.setdefault(phase, "not-started")
438
+ if current_phase:
439
+ phase_states[current_phase] = current_phase_state
440
+ work_category = existing.get("workCategory") or ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
441
+ next_recommended_phase = (
442
+ workflow.get("nextRecommendedPhase")
443
+ or default_next_phase.get(current_phase, ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "unknown"))
444
+ )
445
+ last_completed_phase = workflow.get("lastCompletedPhase") or ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")
446
+ routing_status = workflow.get("routingStatus") or ctx.get("WORKFLOW_ROUTING_STATUS", "not-applicable")
447
+ awaiting_approval = workflow.get("awaitingApproval")
448
+ if not isinstance(awaiting_approval, bool):
449
+ awaiting_approval = ctx.get("WORKFLOW_AWAITING_APPROVAL", "false") == "true"
450
+ render_only = ctx.get("RENDER_ONLY", "") == "true"
451
+ existing_checkpoint = existing.get("workflow", {}).get("lastSafeCheckpoint", {}) if isinstance(existing.get("workflow"), dict) else {}
452
+ if not isinstance(existing_checkpoint, dict):
453
+ existing_checkpoint = {}
454
+ if render_only:
455
+ last_safe_checkpoint = {
456
+ "label": existing_checkpoint.get("label", ""),
457
+ "taskManifestPath": existing_checkpoint.get("taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")),
458
+ "taskIndexPath": existing_checkpoint.get("taskIndexPath", ctx.get("TASK_INDEX_RELATIVE_PATH", "")),
459
+ "latestRunPath": existing_checkpoint.get("latestRunPath", existing.get("latestRunPath", "")),
460
+ "latestRunManifestPath": existing_checkpoint.get("latestRunManifestPath", ""),
461
+ "latestTeamStatePath": existing_checkpoint.get("latestTeamStatePath", existing.get("teamStatePath", "")),
462
+ "latestReportPath": existing_checkpoint.get("latestReportPath", existing.get("latestReportPath", "")),
463
+ "latestResumeCommandPath": existing_checkpoint.get("latestResumeCommandPath", existing.get("latestResumeCommandPath", "")),
464
+ }
465
+ latest_run_relative = existing.get("latestRunPath", "") or ctx.get("LATEST_RUN_RELATIVE_PATH", "")
466
+ latest_run_status = existing.get("latestRunStatus", "") or ctx.get("CURRENT_RUN_STATUS", "")
467
+ latest_run_prompts_relative = existing.get("latestRunPromptsPath", "") or ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
468
+ latest_report_relative = existing.get("latestReportPath", "") or current_report_relative
469
+ latest_team_state_relative = existing.get("teamStatePath", "") or ctx.get("TEAM_STATE_RELATIVE_PATH", "")
470
+ latest_resume_command_relative = existing.get("latestResumeCommandPath", "")
471
+ else:
472
+ last_safe_checkpoint = {
473
+ "label": ctx.get("WORKFLOW_LAST_SAFE_CHECKPOINT_LABEL", ""),
474
+ "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
475
+ "taskIndexPath": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
476
+ "latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
477
+ "latestRunManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
478
+ "latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
479
+ "latestReportPath": current_report_relative,
480
+ "latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
481
+ }
482
+ latest_run_relative = ctx.get("LATEST_RUN_RELATIVE_PATH", "")
483
+ latest_run_status = ctx.get("CURRENT_RUN_STATUS", "")
484
+ latest_run_prompts_relative = ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
485
+ latest_report_relative = current_report_relative or existing.get("latestReportPath", "")
486
+ latest_team_state_relative = ctx.get("TEAM_STATE_RELATIVE_PATH", "")
487
+ latest_resume_command_relative = ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "") or existing.get("latestResumeCommandPath", "")
488
+ payload = {
489
+ "schemaVersion": "1.0",
490
+ "projectId": ctx.get("PROJECT_ID", ""),
491
+ "taskGroup": ctx.get("TASK_GROUP", ""),
492
+ "taskId": ctx.get("TASK_ID", ""),
493
+ "taskKey": ctx.get("TASK_KEY", ""),
494
+ "taskGroupPathSegment": ctx.get("TASK_GROUP_SEGMENT", ""),
495
+ "taskIdPathSegment": ctx.get("TASK_ID_SEGMENT", ""),
496
+ "projectRoot": ctx.get("PROJECT_ROOT", ""),
497
+ "taskType": ctx.get("ANALYSIS_TYPE", ""),
498
+ "workCategory": work_category,
499
+ "taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
500
+ "recommendedWorkers": reviewers,
501
+ "relatedTasks": related_tasks,
502
+ "currentStatus": ctx.get("CURRENT_TASK_STATUS", ""),
503
+ "renderOnly": ctx.get("RENDER_ONLY", ""),
504
+ "taskRootPath": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
505
+ "instructionSetPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", ""),
506
+ "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
507
+ "taskIndexPath": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
508
+ "runsPath": ctx.get("RUNS_RELATIVE_PATH", ""),
509
+ "historyPath": ctx.get("HISTORY_RELATIVE_PATH", ""),
510
+ "historyTimelinePath": ctx.get("TIMELINE_RELATIVE_PATH", ""),
511
+ "taskCatalogPath": ctx.get("OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""),
512
+ "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
513
+ "latestRunPath": latest_run_relative,
514
+ "latestRunStatus": latest_run_status,
515
+ "latestRunPromptsPath": latest_run_prompts_relative,
516
+ "latestReportPath": latest_report_relative,
517
+ "latestResumeCommandPath": latest_resume_command_relative,
518
+ "teamStatePath": latest_team_state_relative,
519
+ "workflow": {
520
+ "phaseSequence": phase_sequence,
521
+ "currentPhase": current_phase,
522
+ "currentPhaseState": phase_states.get(current_phase, current_phase_state),
523
+ "phaseStates": phase_states,
524
+ "lastCompletedPhase": last_completed_phase,
525
+ "nextRecommendedPhase": next_recommended_phase,
526
+ "awaitingApproval": awaiting_approval,
527
+ "routingStatus": routing_status,
528
+ "lastSafeCheckpoint": last_safe_checkpoint,
529
+ },
530
+ "artifacts": {
531
+ "analysisProfilePath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/analysis-profile.md",
532
+ "analysisMaterialPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/analysis-material.md",
533
+ "taskBriefCopyPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/task-brief.md",
534
+ "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
535
+ "claudeExecutionPromptPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/claude-execution-prompt.md",
536
+ "leadPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
537
+ "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
538
+ "workerPromptPathByWorkerId": worker_prompt_paths,
539
+ "finalReportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
540
+ "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
541
+ "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
542
+ "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
543
+ "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
544
+ },
545
+ "resultContract": {
546
+ "leadAgent": "claude",
547
+ "leadRole": "Claude lead",
548
+ "leadModel": ctx.get("LEAD_MODEL_DISPLAY", ""),
549
+ "leadModelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
550
+ "leadExecutionMode": "synthesis-only",
551
+ "finalSynthesisOwner": "Claude lead",
552
+ "artifactFirst": True,
553
+ "resultCollectionMode": "claude-managed",
554
+ "finalReportFormat": "markdown",
555
+ "finalReportFilename": ctx.get("FINAL_REPORT_FILENAME", ""),
556
+ "finalStatusFilename": ctx.get("FINAL_STATUS_FILENAME", ""),
557
+ "minimumPreferredWorkerResults": len(reviewers),
558
+ "requiredWorkerAttempts": reviewers,
559
+ "requiredWorkerRoles": required_worker_roles,
560
+ "requiredAgentStatusEntries": required_agent_status_entries,
561
+ "requireDistinctLeadFromClaudeWorker": True,
562
+ "requireAllRequiredWorkerAttempts": True,
563
+ "requireGeminiWorkerAttempt": "gemini" in reviewers,
564
+ "requireCollectedWorkerStatusesBeforeFinalVerdict": True,
565
+ "disallowLeadSoloAnalysisAsWorkerResult": True,
566
+ "disallowGenericParallelOnlyExecution": True,
567
+ "workerOutputSections": [
568
+ "Findings", "Missing Information or Assumptions",
569
+ "Safe or Reasonable Areas", "Uncertain Points",
570
+ "Recommended Next Actions",
571
+ ],
572
+ "finalReportSections": [
573
+ "Problem or Validation Summary", "Agent Execution Status",
574
+ "Cross Verification Result", "Final Verdict",
575
+ "Evidence and Detailed Analysis",
576
+ "Missing Information and Risk", "Recommended Next Actions",
577
+ ],
578
+ "statusLabels": [
579
+ "prepared", "team-created", "workers-dispatched",
580
+ "worker-results-collected", "synthesis-written", "in-progress",
581
+ "completed", "contract-violated", "timeout", "error", "not-run",
582
+ ],
583
+ },
584
+ "contractValidation": {
585
+ "required": True,
586
+ "status": ctx.get("VALIDATION_STATUS", "not-run"),
587
+ "lastCheckedAt": ctx.get("VALIDATION_UPDATED_AT", ""),
588
+ "passed": ctx.get("VALIDATION_STATUS", "not-run") == "passed",
589
+ "failures": json.loads(ctx.get("VALIDATION_FAILURES_JSON", "[]")),
590
+ "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
591
+ "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
592
+ },
593
+ "claudeSession": {
594
+ "sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
595
+ "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
596
+ },
597
+ "createdAt": existing.get("createdAt") or ctx.get("RUN_TIMESTAMP_ISO", ""),
598
+ "updatedAt": ctx.get("RUN_TIMESTAMP_ISO", ""),
599
+ }
600
+ _write_json(path, payload)
601
+
602
+
603
+ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
604
+ task_manifest_path = Path(ctx.get("TASK_MANIFEST_FILE", ""))
605
+ task_manifest = {}
606
+ if task_manifest_path.exists():
607
+ try:
608
+ task_manifest = json.loads(task_manifest_path.read_text(encoding="utf-8"))
609
+ except Exception:
610
+ task_manifest = {}
611
+ reviewers = _resolve_workers(ctx)
612
+ catalog = _worker_catalog(ctx)
613
+ required_worker_roles = _required_worker_roles(ctx, reviewers)
614
+ worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
615
+ related_tasks = json.loads(ctx.get("RELATED_TASKS_JSON", "[]"))
616
+ workflow = task_manifest.get("workflow", {}) if isinstance(task_manifest.get("workflow"), dict) else {}
617
+ payload = {
618
+ "schemaVersion": "1.0",
619
+ "projectId": ctx.get("PROJECT_ID", ""),
620
+ "taskGroup": ctx.get("TASK_GROUP", ""),
621
+ "taskId": ctx.get("TASK_ID", ""),
622
+ "taskKey": ctx.get("TASK_KEY", ""),
623
+ "taskType": ctx.get("ANALYSIS_TYPE", ""),
624
+ "workCategory": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
625
+ "taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
626
+ "relatedTasks": related_tasks,
627
+ "recommendedWorkers": reviewers,
628
+ "taskRootPath": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
629
+ "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
630
+ "instructionSetPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", ""),
631
+ "taskCatalogPath": ctx.get("OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""),
632
+ "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
633
+ "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
634
+ "runDateTimeSegment": ctx.get("RUN_DATETIME_SEGMENT", ""),
635
+ "runSequencesByCategory": {
636
+ "manifests": ctx.get("RUN_MANIFESTS_SEQ", ""),
637
+ "prompts": ctx.get("RUN_PROMPTS_SEQ", ""),
638
+ "reports": ctx.get("RUN_REPORTS_SEQ", ""),
639
+ "status": ctx.get("RUN_STATUS_SEQ", ""),
640
+ "state": ctx.get("RUN_STATE_SEQ", ""),
641
+ "sessions": ctx.get("RUN_SESSIONS_SEQ", ""),
642
+ "workerResults": ctx.get("WORKER_RESULTS_SEQ", ""),
643
+ },
644
+ "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
645
+ "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
646
+ "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
647
+ "workerPromptPathByWorkerId": worker_prompt_paths,
648
+ "expectedReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
649
+ "expectedStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
650
+ "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
651
+ "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
652
+ "reportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
653
+ "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
654
+ "claudeSessionId": ctx.get("CLAUDE_SESSION_ID", ""),
655
+ "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
656
+ "workflowSnapshot": {
657
+ "phaseSequence": workflow.get("phaseSequence", []),
658
+ "currentPhase": workflow.get("currentPhase", ctx.get("WORKFLOW_CURRENT_PHASE", "")),
659
+ "currentPhaseState": workflow.get("currentPhaseState", ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "")),
660
+ "phaseStates": workflow.get("phaseStates", {}),
661
+ "lastCompletedPhase": workflow.get("lastCompletedPhase", ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")),
662
+ "nextRecommendedPhase": workflow.get("nextRecommendedPhase", ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "")),
663
+ "awaitingApproval": workflow.get("awaitingApproval", ctx.get("WORKFLOW_AWAITING_APPROVAL", "false") == "true"),
664
+ "routingStatus": workflow.get("routingStatus", ctx.get("WORKFLOW_ROUTING_STATUS", "")),
665
+ "lastSafeCheckpoint": workflow.get("lastSafeCheckpoint", {}),
666
+ },
667
+ "teamContract": {
668
+ "leadAgent": "claude",
669
+ "leadRole": "Claude lead",
670
+ "leadModel": ctx.get("LEAD_MODEL_DISPLAY", ""),
671
+ "leadModelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
672
+ "leadExecutionMode": "synthesis-only",
673
+ "finalSynthesisOwner": "Claude lead",
674
+ "requiredWorkerAttempts": reviewers,
675
+ "requiredWorkerRoles": required_worker_roles,
676
+ "requiredAgentStatusEntries": ["Claude lead"] + [catalog[item]["role"] for item in reviewers],
677
+ "requireDistinctLeadFromClaudeWorker": True,
678
+ "requireAllRequiredWorkerAttempts": True,
679
+ "requireGeminiWorkerAttempt": "gemini" in reviewers,
680
+ "requireCollectedWorkerStatusesBeforeFinalVerdict": True,
681
+ "disallowLeadSoloAnalysisAsWorkerResult": True,
682
+ "disallowGenericParallelOnlyExecution": True,
683
+ "preferredCompletedWorkerResults": len(reviewers),
684
+ },
685
+ "validation": {
686
+ "required": True,
687
+ "status": ctx.get("VALIDATION_STATUS", "not-run"),
688
+ "lastCheckedAt": ctx.get("VALIDATION_UPDATED_AT", ""),
689
+ "passed": ctx.get("VALIDATION_STATUS", "not-run") == "passed",
690
+ "failures": json.loads(ctx.get("VALIDATION_FAILURES_JSON", "[]")),
691
+ },
692
+ "status": ctx.get("CURRENT_RUN_STATUS", ""),
693
+ "renderOnly": ctx.get("RENDER_ONLY", ""),
694
+ "createdAt": ctx.get("RUN_TIMESTAMP_ISO", ""),
695
+ }
696
+ _write_json(Path(run_manifest_path), payload)
697
+
698
+
699
+ def render_timeline(timeline_path: str, ctx: dict) -> None:
700
+ task_manifest_path = Path(ctx.get("TASK_MANIFEST_FILE", ""))
701
+ task_manifest = {}
702
+ if task_manifest_path.exists():
703
+ try:
704
+ task_manifest = json.loads(task_manifest_path.read_text(encoding="utf-8"))
705
+ except Exception:
706
+ task_manifest = {}
707
+ reviewers = _resolve_workers(ctx)
708
+ worker_prompt_paths = {
709
+ "claude": ctx.get("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", ""),
710
+ "codex": ctx.get("CODEX_WORKER_PROMPT_RELATIVE_PATH", ""),
711
+ "gemini": ctx.get("GEMINI_WORKER_PROMPT_RELATIVE_PATH", ""),
712
+ "report-writer": ctx.get("REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH", ""),
713
+ }
714
+ path = Path(timeline_path)
715
+ existing = {}
716
+ if path.exists():
717
+ try:
718
+ existing = json.loads(path.read_text(encoding="utf-8"))
719
+ except Exception:
720
+ existing = {}
721
+ runs = existing.get("runs", [])
722
+ current_run_manifest_path = ctx.get("RUN_MANIFEST_FILE", "")
723
+ current_run_manifest_relative_path = ctx.get("RUN_MANIFEST_RELATIVE_PATH", "")
724
+ filtered = [
725
+ item for item in runs
726
+ if item.get("runManifestPath") != current_run_manifest_relative_path
727
+ and item.get("runManifestPath") != current_run_manifest_path
728
+ ]
729
+ workflow = task_manifest.get("workflow") if isinstance(task_manifest.get("workflow"), dict) else {}
730
+ workflow = workflow or {}
731
+ filtered.append({
732
+ "runTimestamp": ctx.get("RUN_TIMESTAMP_ISO", ""),
733
+ "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
734
+ "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
735
+ "runDateTimeSegment": ctx.get("RUN_DATETIME_SEGMENT", ""),
736
+ "runSequencesByCategory": {
737
+ "manifests": ctx.get("RUN_MANIFESTS_SEQ", ""),
738
+ "prompts": ctx.get("RUN_PROMPTS_SEQ", ""),
739
+ "reports": ctx.get("RUN_REPORTS_SEQ", ""),
740
+ "status": ctx.get("RUN_STATUS_SEQ", ""),
741
+ "state": ctx.get("RUN_STATE_SEQ", ""),
742
+ "sessions": ctx.get("RUN_SESSIONS_SEQ", ""),
743
+ "workerResults": ctx.get("WORKER_RESULTS_SEQ", ""),
744
+ },
745
+ "taskType": ctx.get("ANALYSIS_TYPE", ""),
746
+ "workCategory": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
747
+ "status": ctx.get("CURRENT_RUN_STATUS", ""),
748
+ "taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
749
+ "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
750
+ "workerPromptDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
751
+ "workerPromptPathByWorkerId": {item: worker_prompt_paths[item] for item in reviewers},
752
+ "reportPath": ctx.get("LATEST_REPORT_RELATIVE_PATH") or ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
753
+ "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
754
+ "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
755
+ "relatedTasks": json.loads(ctx.get("RELATED_TASKS_JSON", "[]")),
756
+ "workflowSnapshot": {
757
+ "phaseSequence": workflow.get("phaseSequence", []),
758
+ "currentPhase": workflow.get("currentPhase", ""),
759
+ "currentPhaseState": workflow.get("currentPhaseState", ""),
760
+ "phaseStates": workflow.get("phaseStates", {}),
761
+ "lastCompletedPhase": workflow.get("lastCompletedPhase", ""),
762
+ "nextRecommendedPhase": workflow.get("nextRecommendedPhase", ""),
763
+ "awaitingApproval": workflow.get("awaitingApproval", False),
764
+ "routingStatus": workflow.get("routingStatus", ""),
765
+ "lastSafeCheckpoint": workflow.get("lastSafeCheckpoint", {}),
766
+ },
767
+ })
768
+ payload = {
769
+ "schemaVersion": "1.0",
770
+ "projectId": ctx.get("PROJECT_ID", ""),
771
+ "taskGroup": ctx.get("TASK_GROUP", ""),
772
+ "taskId": ctx.get("TASK_ID", ""),
773
+ "taskKey": ctx.get("TASK_KEY", ""),
774
+ "runs": filtered,
775
+ }
776
+ _write_json(path, payload)
777
+
778
+
779
+ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
780
+ template = Path(template_path).read_text(encoding="utf-8")
781
+ task_manifest_path = Path(ctx["TASK_MANIFEST_FILE"])
782
+ task_manifest = {}
783
+ if task_manifest_path.exists():
784
+ try:
785
+ task_manifest = json.loads(task_manifest_path.read_text(encoding="utf-8"))
786
+ except Exception:
787
+ task_manifest = {}
788
+ workflow = task_manifest.get("workflow", {}) if isinstance(task_manifest.get("workflow"), dict) else {}
789
+ phase_states = workflow.get("phaseStates", {}) if isinstance(workflow.get("phaseStates"), dict) else {}
790
+ phase_order = workflow.get("phaseSequence", [])
791
+ if not isinstance(phase_order, list) or not phase_order:
792
+ phase_order = [
793
+ "requirements-discovery", "error-analysis",
794
+ "implementation-planning", "implementation", "final-verification",
795
+ ]
796
+ phase_state_lines = [
797
+ f"- `{phase}`: `{phase_states.get(phase, 'not-started')}`"
798
+ for phase in phase_order
799
+ ]
800
+ checkpoint = workflow.get("lastSafeCheckpoint", {}) if isinstance(workflow.get("lastSafeCheckpoint"), dict) else {}
801
+ checkpoint_lines = [
802
+ f"- Label: `{checkpoint.get('label', 'unknown')}`",
803
+ f"- Run manifest: `{checkpoint.get('latestRunManifestPath', ctx.get('RUN_MANIFEST_RELATIVE_PATH', ''))}`",
804
+ f"- Team state: `{checkpoint.get('latestTeamStatePath', ctx.get('TEAM_STATE_RELATIVE_PATH', ''))}`",
805
+ f"- Report: `{checkpoint.get('latestReportPath', task_manifest.get('latestReportPath', '--')) or '--'}`",
806
+ f"- Resume command: `{checkpoint.get('latestResumeCommandPath', task_manifest.get('latestResumeCommandPath', '--')) or '--'}`",
807
+ ]
808
+ rc = task_manifest.get("resultContract") if isinstance(task_manifest.get("resultContract"), dict) else {}
809
+ cv = task_manifest.get("contractValidation") if isinstance(task_manifest.get("contractValidation"), dict) else {}
810
+ art = task_manifest.get("artifacts") if isinstance(task_manifest.get("artifacts"), dict) else {}
811
+ mapping = {
812
+ "{{TASK_KEY}}": task_manifest.get("taskKey", ctx.get("TASK_KEY", "")),
813
+ "{{TASK_TYPE}}": task_manifest.get("taskType", ctx.get("ANALYSIS_TYPE", "")),
814
+ "{{CURRENT_TASK_STATUS}}": task_manifest.get("currentStatus", ctx.get("CURRENT_TASK_STATUS", "")),
815
+ "{{CURRENT_RUN_STATUS}}": task_manifest.get("latestRunStatus", ctx.get("CURRENT_RUN_STATUS", "")),
816
+ "{{RELATED_TASKS_INLINE}}": ctx.get("RELATED_TASKS_INLINE", "None"),
817
+ "{{RECOMMENDED_ANALYSERS}}": ", ".join(task_manifest.get("recommendedWorkers", [])),
818
+ "{{LEAD_MODEL}}": rc.get("leadModel", ctx.get("LEAD_MODEL_DISPLAY", "")),
819
+ "{{LATEST_RUN_RELATIVE_PATH}}": task_manifest.get("latestRunPath", ctx.get("LATEST_RUN_RELATIVE_PATH", "")),
820
+ "{{LATEST_REPORT_RELATIVE_PATH}}": task_manifest.get("latestReportPath", ctx.get("LATEST_REPORT_RELATIVE_PATH", "")),
821
+ "{{TEAM_STATE_RELATIVE_PATH}}": task_manifest.get("teamStatePath", ctx.get("TEAM_STATE_RELATIVE_PATH", "")),
822
+ "{{VALIDATION_STATUS}}": cv.get("status", ctx.get("VALIDATION_STATUS", "not-run")),
823
+ "{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": task_manifest.get("latestResumeCommandPath", ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "")),
824
+ "{{MODEL_ASSIGNMENT_LINES}}": "\n".join([
825
+ f"- `Claude lead`: `{rc.get('leadModel', ctx.get('LEAD_MODEL_DISPLAY', ''))}`",
826
+ f"- `Claude worker`: `{ctx.get('CLAUDE_WORKER_MODEL_DISPLAY', '')}`",
827
+ f"- `Codex worker`: `{ctx.get('CODEX_WORKER_MODEL_DISPLAY', '')}`",
828
+ f"- `Gemini worker`: `{ctx.get('GEMINI_WORKER_MODEL_DISPLAY', '')}`",
829
+ f"- `Report writer worker`: `{ctx.get('REPORT_WRITER_MODEL_DISPLAY', '')}`",
830
+ ]),
831
+ "{{TASK_MANIFEST_RELATIVE_PATH}}": task_manifest.get("taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")),
832
+ "{{OKSTRA_LATEST_TASK_RELATIVE_PATH}}": ctx.get("OKSTRA_LATEST_TASK_RELATIVE_PATH", ""),
833
+ "{{INSTRUCTION_SET_RELATIVE_PATH}}": task_manifest.get("instructionSetPath", ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")),
834
+ "{{REFERENCE_EXPECTATIONS_RELATIVE_PATH}}": task_manifest.get("referenceExpectationsPath", ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", "")),
835
+ "{{FINAL_REPORT_TEMPLATE_RELATIVE_PATH}}": art.get("finalReportTemplatePath", ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", "")),
836
+ "{{RUN_MANIFESTS_RELATIVE_PATH}}": ctx.get("RUN_MANIFESTS_RELATIVE_PATH", ""),
837
+ "{{RUN_STATE_RELATIVE_PATH}}": ctx.get("RUN_STATE_RELATIVE_PATH", ""),
838
+ "{{RUN_PROMPTS_RELATIVE_PATH}}": task_manifest.get("latestRunPromptsPath", ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")),
839
+ "{{RUN_REPORTS_RELATIVE_PATH}}": ctx.get("RUN_REPORTS_RELATIVE_PATH", ""),
840
+ "{{RUN_STATUS_RELATIVE_PATH}}": ctx.get("RUN_STATUS_RELATIVE_PATH", ""),
841
+ "{{RUN_SESSIONS_RELATIVE_PATH}}": ctx.get("RUN_SESSIONS_RELATIVE_PATH", ""),
842
+ "{{WORKER_RESULTS_RELATIVE_PATH}}": art.get("workerResultsDirectoryPath", ctx.get("WORKER_RESULTS_RELATIVE_PATH", "")),
843
+ "{{RUN_VALIDATOR_RELATIVE_PATH}}": cv.get("validatorScriptPath", ctx.get("RUN_VALIDATOR_RELATIVE_PATH", "")),
844
+ "{{WORK_CATEGORY}}": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
845
+ "{{WORKFLOW_CURRENT_PHASE}}": workflow.get("currentPhase", ctx.get("WORKFLOW_CURRENT_PHASE", "")),
846
+ "{{WORKFLOW_CURRENT_PHASE_STATE}}": workflow.get("currentPhaseState", ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "")),
847
+ "{{WORKFLOW_LAST_COMPLETED_PHASE}}": workflow.get("lastCompletedPhase", ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")) or "--",
848
+ "{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}": workflow.get("nextRecommendedPhase", ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "")),
849
+ "{{WORKFLOW_AWAITING_APPROVAL}}": "yes" if workflow.get("awaitingApproval", False) else "no",
850
+ "{{WORKFLOW_ROUTING_STATUS}}": workflow.get("routingStatus", ctx.get("WORKFLOW_ROUTING_STATUS", "")),
851
+ "{{WORKFLOW_PHASE_STATE_LINES}}": "\n".join(phase_state_lines),
852
+ "{{WORKFLOW_LAST_SAFE_CHECKPOINT_LINES}}": "\n".join(checkpoint_lines),
853
+ }
854
+ rendered = template
855
+ for k, v in mapping.items():
856
+ rendered = rendered.replace(k, v)
857
+ _write_text(Path(output_path), rendered.rstrip() + "\n")
858
+
859
+
860
+ # --------------------------------------------------------------------------- #
861
+ # launch.template.md rendering
862
+ # --------------------------------------------------------------------------- #
863
+
864
+
865
+ def render_template_file(template_path: str, output_path: str, ctx: dict) -> None:
866
+ template = Path(template_path).read_text(encoding="utf-8")
867
+ selected = _resolve_workers(ctx)
868
+ catalog = _worker_catalog(ctx)
869
+ lead_model = ctx.get("LEAD_MODEL_DISPLAY", "")
870
+ lead_model_execution = ctx.get("LEAD_MODEL_EXECUTION_VALUE", "")
871
+
872
+ def fmt_assignment(role: str, model: str, execution: str) -> str:
873
+ if execution and execution != model:
874
+ return f"- `{role}`: `{model}` (launch value: `{execution}`)"
875
+ return f"- `{role}`: `{model}`"
876
+
877
+ worker_result_lines = []
878
+ team_role_lines = [f" 1. `Claude lead` (assigned model: `{lead_model}`)"]
879
+ model_assignment_lines = [fmt_assignment("Claude lead", lead_model, lead_model_execution)]
880
+ worker_role_labels = []
881
+ execution_status_entries = ["`Claude lead`"]
882
+ execution_status_table_lines = [
883
+ "| 에이전트 | 역할 | 모델 | 상태 | 핵심 발견 요약 |",
884
+ "|----------|------|------|------|----------------|",
885
+ f"| Claude Code | Claude lead | {lead_model} | completed / timeout / error / not-run | 최종 synthesis 작성 상태와 핵심 판단 |",
886
+ ]
887
+ for index, worker in enumerate(selected, start=2):
888
+ m = catalog[worker]
889
+ worker_result_lines.append(
890
+ f"- {m['role']} result path: `{m['resultPath']}` (assigned model: `{m['model']}`)")
891
+ team_role_lines.append(f" {index}. `{m['role']}` (assigned model: `{m['model']}`)")
892
+ model_assignment_lines.append(fmt_assignment(m["role"], m["model"], m["modelExecutionValue"]))
893
+ worker_role_labels.append(f"`{m['role']}`")
894
+ execution_status_entries.append(f"`{m['role']}`")
895
+ execution_status_table_lines.append(
896
+ f"| {m['agentLabel']} | {m['role']} | {m['model']} | completed / timeout / error / not-run | {m['role']}의 핵심 발견 요약 |")
897
+
898
+ if worker_role_labels:
899
+ if len(worker_role_labels) == 1:
900
+ worker_role_sentence = f"- {worker_role_labels[0]} is the required worker role."
901
+ else:
902
+ worker_role_sentence = (
903
+ f"- {', '.join(worker_role_labels[:-1])}, "
904
+ f"and {worker_role_labels[-1]} are the required worker roles.")
905
+ preferred_results_sentence = (
906
+ f"- Aim to collect completed results from all "
907
+ f"{len(worker_role_labels)} required workers.")
908
+ else:
909
+ worker_role_sentence = "- No worker roles were selected for this run."
910
+ preferred_results_sentence = "- No worker results are expected for this run."
911
+ worker_attempt_sentence = (
912
+ "- `Gemini worker` is mandatory to attempt for this workflow."
913
+ if "gemini" in selected
914
+ else "- `Gemini worker` is not selected for this run, so no Gemini attempt is required."
915
+ )
916
+
917
+ mapping = {
918
+ "{{PROJECT_ID}}": ctx.get("PROJECT_ID", ""),
919
+ "{{TASK_GROUP}}": ctx.get("TASK_GROUP", ""),
920
+ "{{TASK_ID}}": ctx.get("TASK_ID", ""),
921
+ "{{TASK_KEY}}": ctx.get("TASK_KEY", ""),
922
+ "{{TASK_TYPE}}": ctx.get("ANALYSIS_TYPE", ""),
923
+ "{{ANALYSIS_PROFILE}}": ctx.get("REVIEW_PROFILE", ""),
924
+ "{{ANALYSIS_TYPE}}": ctx.get("ANALYSIS_TYPE", ""),
925
+ "{{RECOMMENDED_ANALYSERS}}": ctx.get("SELECTED_REVIEWERS", ""),
926
+ "{{PROJECT_ROOT}}": ctx.get("PROJECT_ROOT", ""),
927
+ "{{BRIEF_RELATIVE_PATH}}": ctx.get("BRIEF_RELATIVE_PATH", ""),
928
+ "{{BRIEF_FILE_PATH}}": ctx.get("BRIEF_FILE_PATH", ""),
929
+ "{{CLARIFICATION_RESPONSE_PATH}}": ctx.get("CLARIFICATION_RESPONSE_FILE", ""),
930
+ "{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}": ctx.get("CLARIFICATION_RESPONSE_RELATIVE_PATH", ""),
931
+ "{{RUN_DIR}}": ctx.get("RUN_DIR", ""),
932
+ "{{RUN_DIR_RELATIVE_PATH}}": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
933
+ "{{RUN_MANIFESTS_RELATIVE_PATH}}": ctx.get("RUN_MANIFESTS_RELATIVE_PATH", ""),
934
+ "{{RUN_STATE_RELATIVE_PATH}}": ctx.get("RUN_STATE_RELATIVE_PATH", ""),
935
+ "{{RUN_PROMPTS_RELATIVE_PATH}}": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
936
+ "{{RUN_REPORTS_RELATIVE_PATH}}": ctx.get("RUN_REPORTS_RELATIVE_PATH", ""),
937
+ "{{RUN_STATUS_RELATIVE_PATH}}": ctx.get("RUN_STATUS_RELATIVE_PATH", ""),
938
+ "{{RUN_SESSIONS_RELATIVE_PATH}}": ctx.get("RUN_SESSIONS_RELATIVE_PATH", ""),
939
+ "{{TASK_ROOT}}": ctx.get("TASK_ROOT", ""),
940
+ "{{TASK_MANIFEST_PATH}}": ctx.get("TASK_MANIFEST_FILE", ""),
941
+ "{{TASK_INDEX_PATH}}": ctx.get("TASK_INDEX_FILE", ""),
942
+ "{{INSTRUCTION_SET_PATH}}": ctx.get("INSTRUCTION_SET_DIR", ""),
943
+ "{{RUN_MANIFEST_PATH}}": ctx.get("RUN_MANIFEST_FILE", ""),
944
+ "{{RUN_MANIFEST_RELATIVE_PATH}}": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
945
+ "{{TIMELINE_PATH}}": ctx.get("TIMELINE_FILE", ""),
946
+ "{{RUN_TIMESTAMP_ISO}}": ctx.get("RUN_TIMESTAMP_ISO", ""),
947
+ "{{FINAL_REPORT_PATH}}": ctx.get("FINAL_REPORT_FILE", ""),
948
+ "{{FINAL_REPORT_RELATIVE_PATH}}": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
949
+ "{{FINAL_STATUS_PATH}}": ctx.get("FINAL_STATUS_FILE", ""),
950
+ "{{FINAL_STATUS_RELATIVE_PATH}}": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
951
+ "{{TEAM_STATE_PATH}}": ctx.get("TEAM_STATE_FILE", ""),
952
+ "{{TEAM_STATE_RELATIVE_PATH}}": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
953
+ "{{WORKER_RESULTS_PATH}}": ctx.get("WORKER_RESULTS_DIR", ""),
954
+ "{{WORKER_RESULTS_RELATIVE_PATH}}": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
955
+ "{{RUN_VALIDATOR_PATH}}": ctx.get("RUN_VALIDATOR_SCRIPT", ""),
956
+ "{{RUN_VALIDATOR_RELATIVE_PATH}}": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
957
+ "{{CLAUDE_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("CLAUDE_WORKER_RESULT_RELATIVE_PATH", ""),
958
+ "{{CODEX_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("CODEX_WORKER_RESULT_RELATIVE_PATH", ""),
959
+ "{{GEMINI_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("GEMINI_WORKER_RESULT_RELATIVE_PATH", ""),
960
+ "{{REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", ""),
961
+ "{{LEAD_MODEL}}": lead_model,
962
+ "{{LEAD_MODEL_EXECUTION_VALUE}}": lead_model_execution,
963
+ "{{CLAUDE_WORKER_MODEL}}": ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", ""),
964
+ "{{CLAUDE_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""),
965
+ "{{CODEX_WORKER_MODEL}}": ctx.get("CODEX_WORKER_MODEL_DISPLAY", ""),
966
+ "{{CODEX_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("CODEX_WORKER_MODEL_EXECUTION_VALUE", ""),
967
+ "{{GEMINI_WORKER_MODEL}}": ctx.get("GEMINI_WORKER_MODEL_DISPLAY", ""),
968
+ "{{GEMINI_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("GEMINI_WORKER_MODEL_EXECUTION_VALUE", ""),
969
+ "{{REPORT_WRITER_MODEL}}": ctx.get("REPORT_WRITER_MODEL_DISPLAY", ""),
970
+ "{{REPORT_WRITER_MODEL_EXECUTION_VALUE}}": ctx.get("REPORT_WRITER_MODEL_EXECUTION_VALUE", ""),
971
+ "{{WORKER_RESULT_PATH_LINES}}": "\n".join(worker_result_lines),
972
+ "{{MODEL_ASSIGNMENT_LINES}}": "\n".join(model_assignment_lines),
973
+ "{{TEAM_ROLE_LINES}}": "\n".join(team_role_lines),
974
+ "{{REQUIRED_WORKER_ROLE_SENTENCE}}": worker_role_sentence,
975
+ "{{GEMINI_ATTEMPT_SENTENCE}}": worker_attempt_sentence,
976
+ "{{PREFERRED_WORKER_RESULTS_SENTENCE}}": preferred_results_sentence,
977
+ "{{EXECUTION_STATUS_EXACT_ENTRIES}}": ", ".join(execution_status_entries),
978
+ "{{EXECUTION_STATUS_TABLE_ROWS}}": "\n".join(execution_status_table_lines),
979
+ "{{FINAL_REPORT_TEMPLATE_PATH}}": ctx.get("FINAL_REPORT_TEMPLATE_FILE", ""),
980
+ "{{FINAL_REPORT_TEMPLATE_RELATIVE_PATH}}": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
981
+ "{{REFERENCE_EXPECTATIONS_RELATIVE_PATH}}": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
982
+ "{{CLAUDE_SESSION_ID}}": ctx.get("CLAUDE_SESSION_ID", ""),
983
+ "{{CLAUDE_RESUME_COMMAND_PATH}}": ctx.get("CLAUDE_RESUME_COMMAND_FILE", ""),
984
+ "{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
985
+ "{{TASK_ROOT_RELATIVE_PATH}}": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
986
+ "{{TASK_MANIFEST_RELATIVE_PATH}}": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
987
+ "{{TASK_INDEX_RELATIVE_PATH}}": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
988
+ "{{INSTRUCTION_SET_RELATIVE_PATH}}": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", ""),
989
+ "{{RUNS_RELATIVE_PATH}}": ctx.get("RUNS_RELATIVE_PATH", ""),
990
+ "{{HISTORY_RELATIVE_PATH}}": ctx.get("HISTORY_RELATIVE_PATH", ""),
991
+ "{{OKSTRA_DISCOVERY_RELATIVE_PATH}}": ctx.get("OKSTRA_DISCOVERY_RELATIVE_PATH", ""),
992
+ "{{OKSTRA_LATEST_TASK_RELATIVE_PATH}}": ctx.get("OKSTRA_LATEST_TASK_RELATIVE_PATH", ""),
993
+ "{{OKSTRA_TASK_CATALOG_RELATIVE_PATH}}": ctx.get("OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""),
994
+ "{{LATEST_RUN_RELATIVE_PATH}}": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
995
+ "{{LATEST_REPORT_RELATIVE_PATH}}": ctx.get("LATEST_REPORT_RELATIVE_PATH", ""),
996
+ "{{TIMELINE_RELATIVE_PATH}}": ctx.get("TIMELINE_RELATIVE_PATH", ""),
997
+ "{{CURRENT_TASK_STATUS}}": ctx.get("CURRENT_TASK_STATUS", ""),
998
+ "{{CURRENT_RUN_STATUS}}": ctx.get("CURRENT_RUN_STATUS", ""),
999
+ "{{VALIDATION_STATUS}}": ctx.get("VALIDATION_STATUS", "not-run"),
1000
+ "{{RELATED_TASKS_BULLETS}}": ctx.get("RELATED_TASKS_BULLETS", "- None recorded"),
1001
+ "{{RELATED_TASKS_INLINE}}": ctx.get("RELATED_TASKS_INLINE", "None"),
1002
+ "{{WORKFLOW_CURRENT_PHASE}}": ctx.get("WORKFLOW_CURRENT_PHASE", ""),
1003
+ "{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}": ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", ""),
1004
+ "{{PHASE_ALLOWED_OUTPUTS}}": ctx.get("PHASE_ALLOWED_OUTPUTS", ""),
1005
+ "{{PHASE_FORBIDDEN_ACTIONS}}": ctx.get("PHASE_FORBIDDEN_ACTIONS", ""),
1006
+ }
1007
+ rendered = template
1008
+ for k, v in mapping.items():
1009
+ rendered = rendered.replace(k, v)
1010
+ _write_text(Path(output_path), rendered.rstrip() + "\n")
1011
+
1012
+
1013
+ # --------------------------------------------------------------------------- #
1014
+ # CLI dispatcher
1015
+ # --------------------------------------------------------------------------- #
1016
+
1017
+
1018
+ def main(argv: list[str]) -> int:
1019
+ if not argv:
1020
+ print("usage: python3 -m okstra_ctl.render <subcommand> ...", file=sys.stderr)
1021
+ return 2
1022
+ sub = argv[0]
1023
+ rest = argv[1:]
1024
+ try:
1025
+ if sub == "team-state":
1026
+ ctx_path, team_state_path = rest
1027
+ render_team_state(team_state_path, _load_ctx(ctx_path))
1028
+ elif sub == "reference-expectations":
1029
+ ctx_path, brief_path, output_path = rest
1030
+ render_reference_expectations(brief_path, output_path, _load_ctx(ctx_path))
1031
+ elif sub == "task-catalog-discovery":
1032
+ ctx_path, output_path = rest
1033
+ render_task_catalog_discovery(output_path, _load_ctx(ctx_path))
1034
+ elif sub == "latest-task-discovery":
1035
+ ctx_path, output_path = rest
1036
+ render_latest_task_discovery(output_path, _load_ctx(ctx_path))
1037
+ elif sub == "migrate-legacy":
1038
+ (ctx_path,) = rest
1039
+ migrate_legacy_run_artifacts(_load_ctx(ctx_path))
1040
+ elif sub == "task-manifest":
1041
+ ctx_path, manifest_path = rest
1042
+ render_task_manifest(manifest_path, _load_ctx(ctx_path))
1043
+ elif sub == "run-manifest":
1044
+ ctx_path, run_manifest_path = rest
1045
+ render_run_manifest(run_manifest_path, _load_ctx(ctx_path))
1046
+ elif sub == "timeline":
1047
+ ctx_path, timeline_path = rest
1048
+ render_timeline(timeline_path, _load_ctx(ctx_path))
1049
+ elif sub == "task-index":
1050
+ ctx_path, template_path, output_path = rest
1051
+ render_task_index(template_path, output_path, _load_ctx(ctx_path))
1052
+ elif sub == "template":
1053
+ ctx_path, template_path, output_path = rest
1054
+ render_template_file(template_path, output_path, _load_ctx(ctx_path))
1055
+ else:
1056
+ print(f"unknown subcommand: {sub}", file=sys.stderr)
1057
+ return 2
1058
+ except Exception as exc:
1059
+ print(f"render {sub} failed: {exc}", file=sys.stderr)
1060
+ return 1
1061
+ return 0
1062
+
1063
+
1064
+ if __name__ == "__main__":
1065
+ raise SystemExit(main(sys.argv[1:]))