okstra 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/bin/okstra +62 -0
- package/package.json +30 -0
- package/runtime/.gitkeep +0 -0
- package/runtime/BUILD.json +5 -0
- package/runtime/agents/SKILL.md +243 -0
- package/runtime/agents/TODO.md +168 -0
- package/runtime/agents/workers/claude-worker.md +106 -0
- package/runtime/agents/workers/codex-worker.md +179 -0
- package/runtime/agents/workers/gemini-worker.md +179 -0
- package/runtime/agents/workers/report-writer-worker.md +116 -0
- package/runtime/bin/okstra-central.sh +152 -0
- package/runtime/bin/okstra-codex-exec.sh +53 -0
- package/runtime/bin/okstra-error-log.py +295 -0
- package/runtime/bin/okstra-gemini-exec.sh +55 -0
- package/runtime/bin/okstra-token-usage.py +46 -0
- package/runtime/bin/okstra.sh +162 -0
- package/runtime/prompts/launch.template.md +52 -0
- package/runtime/prompts/profiles/error-analysis.md +43 -0
- package/runtime/prompts/profiles/final-verification.md +37 -0
- package/runtime/prompts/profiles/implementation-planning.md +85 -0
- package/runtime/prompts/profiles/implementation.md +71 -0
- package/runtime/prompts/profiles/requirements-discovery.md +43 -0
- package/runtime/python/lib/okstra/cli.sh +227 -0
- package/runtime/python/lib/okstra/globals.sh +157 -0
- package/runtime/python/lib/okstra/interactive.sh +411 -0
- package/runtime/python/lib/okstra/project-resolver.sh +57 -0
- package/runtime/python/lib/okstra/usage.sh +98 -0
- package/runtime/python/lib/okstra-ctl/cmd-batch.sh +59 -0
- package/runtime/python/lib/okstra-ctl/cmd-list.sh +35 -0
- package/runtime/python/lib/okstra-ctl/cmd-open.sh +36 -0
- package/runtime/python/lib/okstra-ctl/cmd-projects.sh +26 -0
- package/runtime/python/lib/okstra-ctl/cmd-reconcile.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-reindex.sh +38 -0
- package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +326 -0
- package/runtime/python/lib/okstra-ctl/cmd-show.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-tail.sh +76 -0
- package/runtime/python/lib/okstra-ctl/main.sh +41 -0
- package/runtime/python/lib/okstra-ctl/prepare.sh +29 -0
- package/runtime/python/lib/okstra-ctl/usage.sh +23 -0
- package/runtime/python/okstra_ctl/__init__.py +125 -0
- package/runtime/python/okstra_ctl/backfill.py +253 -0
- package/runtime/python/okstra_ctl/batch.py +62 -0
- package/runtime/python/okstra_ctl/ids.py +84 -0
- package/runtime/python/okstra_ctl/index.py +216 -0
- package/runtime/python/okstra_ctl/invocation.py +49 -0
- package/runtime/python/okstra_ctl/jsonl.py +84 -0
- package/runtime/python/okstra_ctl/listing.py +156 -0
- package/runtime/python/okstra_ctl/locks.py +42 -0
- package/runtime/python/okstra_ctl/material.py +62 -0
- package/runtime/python/okstra_ctl/models.py +63 -0
- package/runtime/python/okstra_ctl/path_resolve.py +40 -0
- package/runtime/python/okstra_ctl/paths.py +251 -0
- package/runtime/python/okstra_ctl/project_meta.py +51 -0
- package/runtime/python/okstra_ctl/reconcile.py +166 -0
- package/runtime/python/okstra_ctl/render.py +1065 -0
- package/runtime/python/okstra_ctl/resolver.py +54 -0
- package/runtime/python/okstra_ctl/run.py +674 -0
- package/runtime/python/okstra_ctl/run_context.py +166 -0
- package/runtime/python/okstra_ctl/seeding.py +97 -0
- package/runtime/python/okstra_ctl/sequence.py +53 -0
- package/runtime/python/okstra_ctl/session.py +33 -0
- package/runtime/python/okstra_ctl/tmux.py +27 -0
- package/runtime/python/okstra_ctl/workers.py +64 -0
- package/runtime/python/okstra_ctl/workflow.py +182 -0
- package/runtime/python/okstra_project/__init__.py +41 -0
- package/runtime/python/okstra_project/resolver.py +126 -0
- package/runtime/python/okstra_project/state.py +170 -0
- package/runtime/python/okstra_token_usage/__init__.py +26 -0
- package/runtime/python/okstra_token_usage/blocks.py +62 -0
- package/runtime/python/okstra_token_usage/claude.py +97 -0
- package/runtime/python/okstra_token_usage/cli.py +84 -0
- package/runtime/python/okstra_token_usage/codex.py +80 -0
- package/runtime/python/okstra_token_usage/collect.py +161 -0
- package/runtime/python/okstra_token_usage/gemini.py +77 -0
- package/runtime/python/okstra_token_usage/jsonl_io.py +18 -0
- package/runtime/python/okstra_token_usage/paths.py +22 -0
- package/runtime/python/okstra_token_usage/pricing.py +71 -0
- package/runtime/python/okstra_token_usage/report.py +64 -0
- package/runtime/templates/prd/brief.template.md +273 -0
- package/runtime/templates/project-docs/task-index.template.md +65 -0
- package/runtime/templates/reports/error-analysis-input.template.md +80 -0
- package/runtime/templates/reports/final-report.template.md +167 -0
- package/runtime/templates/reports/final-verification-input.template.md +67 -0
- package/runtime/templates/reports/implementation-input.template.md +81 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +93 -0
- package/runtime/templates/reports/quick-input.template.md +64 -0
- package/runtime/templates/reports/schedule.template.md +168 -0
- package/runtime/templates/reports/settings.template.json +101 -0
- package/runtime/templates/reports/task-brief.template.md +165 -0
- package/runtime/validators/lib/common.sh +44 -0
- package/runtime/validators/lib/fixtures.sh +322 -0
- package/runtime/validators/lib/paths.sh +44 -0
- package/runtime/validators/lib/runners.sh +140 -0
- package/runtime/validators/lib/summary.sh +15 -0
- package/runtime/validators/lib/validate-assets.sh +44 -0
- package/runtime/validators/lib/validate-prompt-metadata.sh +267 -0
- package/runtime/validators/lib/validate-tasks.sh +335 -0
- package/runtime/validators/validate-run.py +568 -0
- package/runtime/validators/validate-schedule.py +665 -0
- package/runtime/validators/validate-workflow.sh +190 -0
- package/src/doctor.mjs +127 -0
- package/src/install.mjs +355 -0
- package/src/paths.mjs +132 -0
- package/src/uninstall.mjs +122 -0
- package/src/version.mjs +20 -0
|
@@ -0,0 +1,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:]))
|