okstra 0.75.0 → 0.77.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.kr.md +5 -3
- package/README.md +5 -3
- package/bin/okstra +10 -2
- package/docs/kr/architecture.md +1 -1
- package/docs/kr/cli.md +20 -4
- package/docs/pr-template-usage.md +3 -3
- package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +1 -1
- package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +358 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/agents/workers/gemini-worker.md +1 -1
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +6 -4
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/launch.template.md +4 -13
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +21 -0
- package/runtime/prompts/profiles/_implementation-executor.md +3 -6
- package/runtime/prompts/profiles/error-analysis.md +6 -0
- package/runtime/prompts/profiles/improvement-discovery.md +5 -0
- package/runtime/prompts/profiles/release-handoff.md +2 -2
- package/runtime/prompts/wizard/prompts.ko.json +12 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +1484 -0
- package/runtime/python/okstra_ctl/lead_events.py +129 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/pr_template.py +1 -1
- package/runtime/python/okstra_ctl/render.py +160 -29
- package/runtime/python/okstra_ctl/run.py +78 -15
- package/runtime/python/okstra_ctl/wizard.py +23 -9
- package/runtime/python/okstra_token_usage/codex.py +12 -7
- package/runtime/python/okstra_token_usage/collect.py +243 -54
- package/runtime/python/okstra_token_usage/gemini.py +12 -7
- package/runtime/skills/okstra-setup/SKILL.md +1 -1
- package/runtime/validators/validate-run.py +56 -22
- package/runtime/validators/validate_session_conformance.py +121 -4
- package/src/codex-dispatch.mjs +70 -0
- package/src/codex-run.mjs +68 -0
- package/src/doctor.mjs +40 -4
- package/src/install.mjs +122 -26
- package/src/paths.mjs +17 -3
- package/src/render-bundle.mjs +2 -0
- package/src/runtime-manifest.mjs +26 -0
- package/src/uninstall.mjs +26 -4
- /package/runtime/{skills/okstra-run/templates → templates/prd}/pr-body.template.md +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Structured JSONL events emitted by non-Claude lead runtimes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
REQUIRED_FIELDS = (
|
|
11
|
+
"eventType",
|
|
12
|
+
"leadRuntime",
|
|
13
|
+
"taskKey",
|
|
14
|
+
"taskType",
|
|
15
|
+
"runSeq",
|
|
16
|
+
"timestamp",
|
|
17
|
+
"details",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LeadEventParseError(ValueError):
|
|
22
|
+
"""Raised when a lead-events JSONL file contains an invalid row."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: Path, line_number: int, message: str) -> None:
|
|
25
|
+
super().__init__(f"{path}:{line_number}: {message}")
|
|
26
|
+
self.path = path
|
|
27
|
+
self.line_number = line_number
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class LeadEvent:
|
|
32
|
+
event_type: str
|
|
33
|
+
lead_runtime: str
|
|
34
|
+
task_key: str
|
|
35
|
+
task_type: str
|
|
36
|
+
run_seq: str
|
|
37
|
+
timestamp: str
|
|
38
|
+
details: Mapping[str, Any] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
def to_record(self) -> dict[str, Any]:
|
|
41
|
+
return {
|
|
42
|
+
"eventType": self.event_type,
|
|
43
|
+
"leadRuntime": self.lead_runtime,
|
|
44
|
+
"taskKey": self.task_key,
|
|
45
|
+
"taskType": self.task_type,
|
|
46
|
+
"runSeq": self.run_seq,
|
|
47
|
+
"timestamp": self.timestamp,
|
|
48
|
+
"details": _stable_json_value(dict(self.details)),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_record(cls, record: Mapping[str, Any]) -> "LeadEvent":
|
|
53
|
+
missing = [field_name for field_name in REQUIRED_FIELDS
|
|
54
|
+
if field_name not in record]
|
|
55
|
+
if missing:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"missing required lead event field(s): " + ", ".join(missing)
|
|
58
|
+
)
|
|
59
|
+
details = record["details"]
|
|
60
|
+
if not isinstance(details, Mapping):
|
|
61
|
+
raise ValueError("lead event details must be a JSON object")
|
|
62
|
+
return cls(
|
|
63
|
+
event_type=_require_string(record, "eventType"),
|
|
64
|
+
lead_runtime=_require_string(record, "leadRuntime"),
|
|
65
|
+
task_key=_require_string(record, "taskKey"),
|
|
66
|
+
task_type=_require_string(record, "taskType"),
|
|
67
|
+
run_seq=_require_string(record, "runSeq"),
|
|
68
|
+
timestamp=_require_string(record, "timestamp"),
|
|
69
|
+
details=dict(details),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def append_lead_event(path: Path, event: LeadEvent) -> None:
|
|
74
|
+
"""Append one lead event as compact JSONL, creating parent directories."""
|
|
75
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
with path.open("a", encoding="utf-8") as f:
|
|
77
|
+
f.write(_event_json(event) + "\n")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_lead_events(path: Path) -> list[LeadEvent]:
|
|
81
|
+
"""Parse a lead-events JSONL file.
|
|
82
|
+
|
|
83
|
+
Missing files read as an empty log. Blank lines are ignored. Malformed JSON
|
|
84
|
+
and malformed event objects fail loudly with path and line number context.
|
|
85
|
+
"""
|
|
86
|
+
if not path.is_file():
|
|
87
|
+
return []
|
|
88
|
+
events: list[LeadEvent] = []
|
|
89
|
+
for line_number, line in enumerate(
|
|
90
|
+
path.read_text(encoding="utf-8").splitlines(), start=1
|
|
91
|
+
):
|
|
92
|
+
stripped = line.strip()
|
|
93
|
+
if not stripped:
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
record = json.loads(stripped)
|
|
97
|
+
except json.JSONDecodeError as exc:
|
|
98
|
+
raise LeadEventParseError(path, line_number, "invalid JSON") from exc
|
|
99
|
+
if not isinstance(record, Mapping):
|
|
100
|
+
raise LeadEventParseError(
|
|
101
|
+
path, line_number, "lead event row must be a JSON object"
|
|
102
|
+
)
|
|
103
|
+
try:
|
|
104
|
+
events.append(LeadEvent.from_record(record))
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
raise LeadEventParseError(path, line_number, str(exc)) from exc
|
|
107
|
+
return events
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _event_json(event: LeadEvent) -> str:
|
|
111
|
+
return json.dumps(event.to_record(), ensure_ascii=False, separators=(",", ":"))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _require_string(record: Mapping[str, Any], field_name: str) -> str:
|
|
115
|
+
value = record[field_name]
|
|
116
|
+
if not isinstance(value, str) or not value:
|
|
117
|
+
raise ValueError(f"lead event {field_name} must be a non-empty string")
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _stable_json_value(value: Any) -> Any:
|
|
122
|
+
if isinstance(value, Mapping):
|
|
123
|
+
return {
|
|
124
|
+
key: _stable_json_value(value[key])
|
|
125
|
+
for key in sorted(value)
|
|
126
|
+
}
|
|
127
|
+
if isinstance(value, list):
|
|
128
|
+
return [_stable_json_value(item) for item in value]
|
|
129
|
+
return value
|
|
@@ -175,6 +175,7 @@ def compute_run_paths(
|
|
|
175
175
|
final_status = run_status / f"final{suffixes['status']}.status"
|
|
176
176
|
team_state = run_state / f"team-state{suffixes['state']}.json"
|
|
177
177
|
active_run_context = run_state / f"active-run-context{suffixes['state']}.json"
|
|
178
|
+
lead_events = run_state / f"lead-events-{task_type_segment}-{seqs['state']}.jsonl"
|
|
178
179
|
final_report_template = instruction_set / "final-report-template.md"
|
|
179
180
|
final_report_schema = instruction_set / "final-report-schema.json"
|
|
180
181
|
reference_expectations = instruction_set / "reference-expectations.md"
|
|
@@ -241,6 +242,7 @@ def compute_run_paths(
|
|
|
241
242
|
"FINAL_STATUS_PATH": str(final_status),
|
|
242
243
|
"TEAM_STATE_PATH": str(team_state),
|
|
243
244
|
"ACTIVE_RUN_CONTEXT_PATH": str(active_run_context),
|
|
245
|
+
"LEAD_EVENTS_PATH": str(lead_events),
|
|
244
246
|
"FINAL_REPORT_TEMPLATE_PATH": str(final_report_template),
|
|
245
247
|
"FINAL_REPORT_SCHEMA_PATH": str(final_report_schema),
|
|
246
248
|
"REFERENCE_EXPECTATIONS_FILE": str(reference_expectations),
|
|
@@ -301,6 +303,7 @@ def compute_run_paths(
|
|
|
301
303
|
("FINAL_STATUS_RELATIVE_PATH", final_status),
|
|
302
304
|
("TEAM_STATE_RELATIVE_PATH", team_state),
|
|
303
305
|
("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", active_run_context),
|
|
306
|
+
("LEAD_EVENTS_RELATIVE_PATH", lead_events),
|
|
304
307
|
("WORKER_RESULTS_RELATIVE_PATH", worker_results),
|
|
305
308
|
("RUN_CARRY_RELATIVE_PATH", run_carry),
|
|
306
309
|
("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", final_report_template),
|
|
@@ -6,7 +6,7 @@ release-handoff 단계에서 lead 가 PR 본문을 작성할 때 사용하는
|
|
|
6
6
|
1. per-run override (okstra-run Step 6 에서 입력)
|
|
7
7
|
2. project: <project_root>/.okstra/project.json 의 ``prTemplatePath``
|
|
8
8
|
3. global: ~/.okstra/config.json 의 ``prTemplatePath``
|
|
9
|
-
4. default: 스킬 설치 디렉터리의 ``
|
|
9
|
+
4. default: 스킬 설치 디렉터리의 ``templates/prd/pr-body.template.md``
|
|
10
10
|
|
|
11
11
|
경로는 절대경로 또는 ``~`` 시작 경로를 권장한다. 상대경로일 경우 project
|
|
12
12
|
스코프는 ``project_root`` 기준, override 는 호출자 cwd 기준으로 해석한다.
|
|
@@ -66,6 +66,37 @@ def _write_json(path: Path, payload: dict) -> None:
|
|
|
66
66
|
_write_text(path, json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _lead_runtime(ctx: dict) -> str:
|
|
70
|
+
return ctx.get("LEAD_RUNTIME", "") or "claude-code"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _lead_agent(ctx: dict) -> str:
|
|
74
|
+
return "codex" if _lead_runtime(ctx) == "codex" else "claude"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _lead_agent_label(ctx: dict) -> str:
|
|
78
|
+
return "Codex CLI" if _lead_runtime(ctx) == "codex" else "Claude Code"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _lead_role(ctx: dict) -> str:
|
|
82
|
+
return "Codex lead" if _lead_runtime(ctx) == "codex" else "Claude lead"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _lead_adapter(ctx: dict) -> dict:
|
|
86
|
+
runtime = _lead_runtime(ctx)
|
|
87
|
+
if runtime == "codex":
|
|
88
|
+
return {
|
|
89
|
+
"name": "codex",
|
|
90
|
+
"dispatchMode": "render-only",
|
|
91
|
+
"sessionAccounting": "artifact-only",
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
"name": "claude-code",
|
|
95
|
+
"dispatchMode": "team",
|
|
96
|
+
"sessionAccounting": "claude-jsonl",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
69
100
|
_PHASE_BLOCK_RE = re.compile(
|
|
70
101
|
r"\{% if header\.taskType == '(implementation-planning|release-handoff|implementation|final-verification)' %\}\n(.*?)\{% endif %\}\n",
|
|
71
102
|
re.DOTALL,
|
|
@@ -415,11 +446,14 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
|
|
|
415
446
|
"schemaVersion": "1.0",
|
|
416
447
|
"taskKey": ctx.get("TASK_KEY", ""),
|
|
417
448
|
"taskType": ctx.get("TASK_TYPE", ""),
|
|
449
|
+
"leadRuntime": _lead_runtime(ctx),
|
|
450
|
+
"leadAdapter": _lead_adapter(ctx),
|
|
451
|
+
"leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
|
|
418
452
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
419
453
|
"workflowState": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
420
454
|
"lead": {
|
|
421
|
-
"role":
|
|
422
|
-
"agent":
|
|
455
|
+
"role": _lead_role(ctx),
|
|
456
|
+
"agent": _lead_agent(ctx),
|
|
423
457
|
"model": ctx.get("LEAD_MODEL", ""),
|
|
424
458
|
"modelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
|
|
425
459
|
"status": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
@@ -876,7 +910,9 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
876
910
|
catalog = _worker_catalog(ctx)
|
|
877
911
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
878
912
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
879
|
-
|
|
913
|
+
lead_agent = _lead_agent(ctx)
|
|
914
|
+
lead_role = _lead_role(ctx)
|
|
915
|
+
required_agent_status_entries = [lead_role] + [
|
|
880
916
|
catalog[item]["role"] for item in reviewers
|
|
881
917
|
]
|
|
882
918
|
related_tasks = json.loads(ctx.get("RELATED_TASKS_JSON", "[]"))
|
|
@@ -925,6 +961,8 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
925
961
|
"taskIdPathSegment": ctx.get("TASK_ID_SEGMENT", ""),
|
|
926
962
|
"projectRoot": ctx.get("PROJECT_ROOT", ""),
|
|
927
963
|
"taskType": ctx.get("TASK_TYPE", ""),
|
|
964
|
+
"leadRuntime": _lead_runtime(ctx),
|
|
965
|
+
"leadAdapter": _lead_adapter(ctx),
|
|
928
966
|
"workCategory": work_category,
|
|
929
967
|
"taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
|
|
930
968
|
"recommendedWorkers": reviewers,
|
|
@@ -984,16 +1022,17 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
984
1022
|
"resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
985
1023
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
986
1024
|
"activeRunContextPath": ctx.get("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", ""),
|
|
1025
|
+
"leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
|
|
987
1026
|
"workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
|
|
988
1027
|
"validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
|
|
989
1028
|
},
|
|
990
1029
|
"resultContract": {
|
|
991
|
-
"leadAgent":
|
|
992
|
-
"leadRole":
|
|
1030
|
+
"leadAgent": lead_agent,
|
|
1031
|
+
"leadRole": lead_role,
|
|
993
1032
|
"leadModel": ctx.get("LEAD_MODEL", ""),
|
|
994
1033
|
"leadModelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
|
|
995
1034
|
"leadExecutionMode": "synthesis-only",
|
|
996
|
-
"finalSynthesisOwner":
|
|
1035
|
+
"finalSynthesisOwner": lead_role,
|
|
997
1036
|
"artifactFirst": True,
|
|
998
1037
|
"resultCollectionMode": "claude-managed",
|
|
999
1038
|
"finalReportFormat": "markdown",
|
|
@@ -1127,6 +1166,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1127
1166
|
catalog = _worker_catalog(ctx)
|
|
1128
1167
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
1129
1168
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
1169
|
+
lead_agent = _lead_agent(ctx)
|
|
1170
|
+
lead_role = _lead_role(ctx)
|
|
1130
1171
|
related_tasks = json.loads(ctx.get("RELATED_TASKS_JSON", "[]"))
|
|
1131
1172
|
workflow = (
|
|
1132
1173
|
task_manifest.get("workflow", {})
|
|
@@ -1149,6 +1190,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1149
1190
|
"taskId": ctx.get("TASK_ID", ""),
|
|
1150
1191
|
"taskKey": ctx.get("TASK_KEY", ""),
|
|
1151
1192
|
"taskType": ctx.get("TASK_TYPE", ""),
|
|
1193
|
+
"leadRuntime": _lead_runtime(ctx),
|
|
1194
|
+
"leadAdapter": _lead_adapter(ctx),
|
|
1152
1195
|
"workCategory": task_manifest.get(
|
|
1153
1196
|
"workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
|
|
1154
1197
|
),
|
|
@@ -1174,6 +1217,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1174
1217
|
"workerResults": ctx.get("WORKER_RESULTS_SEQ", ""),
|
|
1175
1218
|
},
|
|
1176
1219
|
"runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
1220
|
+
"leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
|
|
1177
1221
|
"promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
|
|
1178
1222
|
"workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
|
|
1179
1223
|
"workerPromptPathByWorkerId": worker_prompt_paths,
|
|
@@ -1216,15 +1260,15 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1216
1260
|
"lastSafeCheckpoint": workflow.get("lastSafeCheckpoint", {}),
|
|
1217
1261
|
},
|
|
1218
1262
|
"teamContract": {
|
|
1219
|
-
"leadAgent":
|
|
1220
|
-
"leadRole":
|
|
1263
|
+
"leadAgent": lead_agent,
|
|
1264
|
+
"leadRole": lead_role,
|
|
1221
1265
|
"leadModel": ctx.get("LEAD_MODEL", ""),
|
|
1222
1266
|
"leadModelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
|
|
1223
1267
|
"leadExecutionMode": "synthesis-only",
|
|
1224
|
-
"finalSynthesisOwner":
|
|
1268
|
+
"finalSynthesisOwner": lead_role,
|
|
1225
1269
|
"requiredWorkerAttempts": reviewers,
|
|
1226
1270
|
"requiredWorkerRoles": required_worker_roles,
|
|
1227
|
-
"requiredAgentStatusEntries": [
|
|
1271
|
+
"requiredAgentStatusEntries": [lead_role]
|
|
1228
1272
|
+ [catalog[item]["role"] for item in reviewers],
|
|
1229
1273
|
"requireDistinctLeadFromClaudeWorker": True,
|
|
1230
1274
|
"requireAllRequiredWorkerAttempts": True,
|
|
@@ -1425,6 +1469,12 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
1425
1469
|
if isinstance(task_manifest.get("artifacts"), dict)
|
|
1426
1470
|
else {}
|
|
1427
1471
|
)
|
|
1472
|
+
lead_role = rc.get("leadRole", _lead_role(ctx))
|
|
1473
|
+
latest_resume_command = (
|
|
1474
|
+
task_manifest.get("latestResumeCommandPath")
|
|
1475
|
+
or ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "")
|
|
1476
|
+
or "--"
|
|
1477
|
+
)
|
|
1428
1478
|
mapping = {
|
|
1429
1479
|
"{{TASK_KEY}}": task_manifest.get("taskKey", ctx.get("TASK_KEY", "")),
|
|
1430
1480
|
"{{TASK_TYPE}}": task_manifest.get("taskType", ctx.get("TASK_TYPE", "")),
|
|
@@ -1456,13 +1506,10 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
1456
1506
|
"{{VALIDATION_STATUS}}": cv.get(
|
|
1457
1507
|
"status", ctx.get("VALIDATION_STATUS", "not-run")
|
|
1458
1508
|
),
|
|
1459
|
-
"{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}":
|
|
1460
|
-
"latestResumeCommandPath",
|
|
1461
|
-
ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
1462
|
-
),
|
|
1509
|
+
"{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": latest_resume_command,
|
|
1463
1510
|
"{{MODEL_ASSIGNMENT_LINES}}": "\n".join(
|
|
1464
1511
|
[
|
|
1465
|
-
f"- `
|
|
1512
|
+
f"- `{lead_role}`: `{rc.get('leadModel', ctx.get('LEAD_MODEL', ''))}`",
|
|
1466
1513
|
f"- `Claude worker`: `{ctx.get('CLAUDE_WORKER_MODEL', '')}`",
|
|
1467
1514
|
f"- `Codex worker`: `{ctx.get('CODEX_WORKER_MODEL', '')}`",
|
|
1468
1515
|
f"- `Gemini worker`: `{ctx.get('GEMINI_WORKER_MODEL', '')}`",
|
|
@@ -1597,22 +1644,22 @@ def build_available_mcp_servers_block(project_root: Path) -> str:
|
|
|
1597
1644
|
|
|
1598
1645
|
|
|
1599
1646
|
def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
1600
|
-
"""Populate ctx in-place with
|
|
1647
|
+
"""Populate ctx in-place with derived lead-prompt tokens.
|
|
1601
1648
|
|
|
1602
|
-
Tokens that are not 1:1 with a ctx key (TEAM_CREATION_GATE,
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
EXECUTION_STATUS_TABLE_ROWS) are computed deterministically from ctx
|
|
1607
|
-
so the pure-lookup renderer (`render_template_with_ctx`) can resolve
|
|
1608
|
-
them via plain `ctx[token]` lookup.
|
|
1649
|
+
Tokens that are not 1:1 with a ctx key (TEAM_CREATION_GATE, lead identity
|
|
1650
|
+
blocks, worker dispatch guidance, worker result/status summaries, etc.) are
|
|
1651
|
+
computed deterministically from ctx so the pure-lookup renderer
|
|
1652
|
+
(`render_template_with_ctx`) can resolve them via plain `ctx[token]` lookup.
|
|
1609
1653
|
|
|
1610
|
-
Always overwrites — caller-supplied values for these
|
|
1654
|
+
Always overwrites — caller-supplied values for these computed keys are replaced
|
|
1611
1655
|
on every call. For optional defaults (VALIDATION_STATUS etc.) use the
|
|
1612
1656
|
companion `apply_lead_prompt_defaults` which preserves caller values.
|
|
1613
1657
|
"""
|
|
1614
1658
|
selected = _resolve_workers(ctx)
|
|
1615
1659
|
catalog = _worker_catalog(ctx)
|
|
1660
|
+
lead_runtime = _lead_runtime(ctx)
|
|
1661
|
+
lead_role = _lead_role(ctx)
|
|
1662
|
+
lead_agent_label = _lead_agent_label(ctx)
|
|
1616
1663
|
lead_model = ctx.get("LEAD_MODEL", "")
|
|
1617
1664
|
lead_model_execution = ctx.get("LEAD_MODEL_EXECUTION_VALUE", "")
|
|
1618
1665
|
|
|
@@ -1622,16 +1669,16 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1622
1669
|
return f"- `{role}`: `{model}`"
|
|
1623
1670
|
|
|
1624
1671
|
worker_result_lines: list[str] = []
|
|
1625
|
-
team_role_lines = [f" 1. `
|
|
1672
|
+
team_role_lines = [f" 1. `{lead_role}` (assigned model: `{lead_model}`)"]
|
|
1626
1673
|
model_assignment_lines = [
|
|
1627
|
-
fmt_assignment(
|
|
1674
|
+
fmt_assignment(lead_role, lead_model, lead_model_execution)
|
|
1628
1675
|
]
|
|
1629
1676
|
worker_role_labels: list[str] = []
|
|
1630
|
-
execution_status_entries = ["`
|
|
1677
|
+
execution_status_entries = [f"`{lead_role}`"]
|
|
1631
1678
|
execution_status_table_lines = [
|
|
1632
1679
|
"| 에이전트 | 역할 | 모델 | 상태 | 핵심 발견 요약 |",
|
|
1633
1680
|
"|----------|------|------|------|----------------|",
|
|
1634
|
-
f"|
|
|
1681
|
+
f"| {lead_agent_label} | {lead_role} | {lead_model} | completed / timeout / error / not-run | 최종 synthesis 작성 상태와 핵심 판단 |",
|
|
1635
1682
|
]
|
|
1636
1683
|
for index, worker in enumerate(selected, start=2):
|
|
1637
1684
|
m = catalog[worker]
|
|
@@ -1682,18 +1729,50 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1682
1729
|
# - All other phases keep the full team-creation contract.
|
|
1683
1730
|
task_type = ctx.get("TASK_TYPE", "")
|
|
1684
1731
|
concurrent_stages = str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").strip()
|
|
1732
|
+
codex_dispatch_command = (
|
|
1733
|
+
"okstra codex-dispatch "
|
|
1734
|
+
f"--project-root {ctx.get('PROJECT_ROOT', '')} "
|
|
1735
|
+
f"--run-manifest {ctx.get('RUN_MANIFEST_RELATIVE_PATH', '')}"
|
|
1736
|
+
)
|
|
1685
1737
|
if task_type == "release-handoff" or not selected:
|
|
1686
1738
|
team_creation_gate_block = (
|
|
1687
1739
|
"## Single-Lead Phase (no team creation)\n"
|
|
1688
1740
|
"\n"
|
|
1689
1741
|
"This run is single-lead. There is no worker roster, no\n"
|
|
1690
1742
|
"`TeamCreate` call, no `Agent(...)` worker dispatch, and no\n"
|
|
1691
|
-
"convergence loop. The
|
|
1743
|
+
f"convergence loop. The {lead_role} performs every step inline\n"
|
|
1692
1744
|
"(reading inputs, drafting commit / PR text when applicable,\n"
|
|
1693
1745
|
"asking the user, running git / gh, and writing the final\n"
|
|
1694
1746
|
"report). Do NOT call `TeamCreate` or dispatch any sub-agent\n"
|
|
1695
1747
|
"from this run — that would be a contract violation."
|
|
1696
1748
|
)
|
|
1749
|
+
elif lead_runtime == "codex":
|
|
1750
|
+
team_creation_gate_block = (
|
|
1751
|
+
"## Codex Dispatch Gate (BLOCKING)\n"
|
|
1752
|
+
"\n"
|
|
1753
|
+
"This run was prepared for `leadRuntime=codex`. Do NOT call\n"
|
|
1754
|
+
"`TeamCreate`, do NOT call Claude Code `Agent(...)`, and do NOT\n"
|
|
1755
|
+
"invoke Claude slash skills. Codex dispatch is artifact-first and\n"
|
|
1756
|
+
"uses the prepared run manifest plus CLI-backed worker wrappers.\n"
|
|
1757
|
+
"\n"
|
|
1758
|
+
"Required actions, in order:\n"
|
|
1759
|
+
"\n"
|
|
1760
|
+
"1. Inspect the run manifest and team-state paths listed below.\n"
|
|
1761
|
+
"2. Emit `PROGRESS: phase-3-team-create skipped (codex-dispatch)`;\n"
|
|
1762
|
+
" there is no Claude Teams surface in this adapter.\n"
|
|
1763
|
+
"3. Plan worker execution with:\n"
|
|
1764
|
+
f" `{codex_dispatch_command} --dry-run`\n"
|
|
1765
|
+
"4. Dispatch supported Codex-side workers with:\n"
|
|
1766
|
+
f" `{codex_dispatch_command}`\n"
|
|
1767
|
+
" When `--workers` is omitted, unsupported roster entries such as\n"
|
|
1768
|
+
" `claude` are skipped with explicit `not-run` reasons.\n"
|
|
1769
|
+
"5. Run `report-writer` only with explicit opt-in:\n"
|
|
1770
|
+
f" `{codex_dispatch_command} --enable-codex-report-writer --report-writer-codex-model <model>`\n"
|
|
1771
|
+
"\n"
|
|
1772
|
+
"`okstra codex-dispatch` records `team-state.dispatchMode`, writes\n"
|
|
1773
|
+
"worker prompt histories when missing, persists worker results, and\n"
|
|
1774
|
+
"runs post-report validation for the Codex report-writer path."
|
|
1775
|
+
)
|
|
1697
1776
|
elif task_type == "implementation" and concurrent_stages:
|
|
1698
1777
|
team_creation_gate_block = (
|
|
1699
1778
|
"## Concurrent-run: no-team background (BLOCKING)\n"
|
|
@@ -1773,8 +1852,60 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1773
1852
|
"response is to go back to step 2 — NOT to strip `team_name` and retry."
|
|
1774
1853
|
)
|
|
1775
1854
|
|
|
1855
|
+
if lead_runtime == "codex":
|
|
1856
|
+
lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
|
|
1857
|
+
lead_bootstrap_instruction = (
|
|
1858
|
+
"Read the manifests below for all task metadata, paths, model "
|
|
1859
|
+
"assignments, and worker roster. Use the Codex dispatch gate in this "
|
|
1860
|
+
"prompt; do not invoke Claude Code skills."
|
|
1861
|
+
)
|
|
1862
|
+
lead_session_block = (
|
|
1863
|
+
"## Session\n"
|
|
1864
|
+
"\n"
|
|
1865
|
+
"- Lead runtime: `codex`\n"
|
|
1866
|
+
"- Codex run resume is artifact/checkpoint based; `okstra codex-run` "
|
|
1867
|
+
"does not create a Claude session id."
|
|
1868
|
+
)
|
|
1869
|
+
worker_dispatch_guidance = (
|
|
1870
|
+
"## Codex Worker Dispatch\n"
|
|
1871
|
+
"\n"
|
|
1872
|
+
"- Worker prompt paths in `task-manifest.json` are assigned history "
|
|
1873
|
+
"locations. `okstra codex-dispatch` materializes missing prompt files "
|
|
1874
|
+
"from the run manifest and active-run-context; existing prompt files "
|
|
1875
|
+
"are never overwritten.\n"
|
|
1876
|
+
"- Do not call `TeamCreate` or `Agent(...)`. Use the command in the "
|
|
1877
|
+
"Codex Dispatch Gate above for worker execution.\n"
|
|
1878
|
+
"- File presence is not a signal to skip. Worker selection and skipped "
|
|
1879
|
+
"statuses come from `team-state`, never from the prompts directory."
|
|
1880
|
+
)
|
|
1881
|
+
else:
|
|
1882
|
+
lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
|
|
1883
|
+
lead_bootstrap_instruction = (
|
|
1884
|
+
"Invoke the `okstra` skill now. Read the manifests below for all task "
|
|
1885
|
+
"metadata, paths, model assignments, and worker roster."
|
|
1886
|
+
)
|
|
1887
|
+
lead_session_block = (
|
|
1888
|
+
"## Session\n"
|
|
1889
|
+
"\n"
|
|
1890
|
+
f"- Session ID: `{ctx.get('CLAUDE_SESSION_ID', '')}`\n"
|
|
1891
|
+
f"- Resume: `{ctx.get('CLAUDE_RESUME_COMMAND_RELATIVE_PATH', '')}`"
|
|
1892
|
+
)
|
|
1893
|
+
worker_dispatch_guidance = (
|
|
1894
|
+
"## Worker Prompt Files (not pre-rendered)\n"
|
|
1895
|
+
"\n"
|
|
1896
|
+
"- The `runs/<phase>/prompts/<worker>-worker-prompt-*.md` files referenced in `task-manifest.json → artifacts.workerPromptPathByWorkerId` are **NOT** rendered by the okstra runtime. Those paths are the *assigned* locations where the prompt history MUST be written at dispatch time.\n"
|
|
1897
|
+
"- Therefore: at the start of every phase the prompts/ directory is normally empty (or contains only previously-dispatched workers' files). This is expected. Do NOT narrate it as \"missing\", \"누락\", or \"not yet rendered\" — it just means dispatch has not happened yet.\n"
|
|
1898
|
+
"- Before dispatching any required worker, **you (the lead) construct the worker prompt and persist it to the assigned absolute path using `Write`** (per `okstra-team-contract` rule 6). Only after persisting do you call the worker subagent (Agent tool / Codex / Gemini wrapper). The wrapper subagents will also re-write the same file on their end; the double-write is intentional and idempotent.\n"
|
|
1899
|
+
"- Do not \"check if the file exists and skip dispatch\" — file presence is not a signal to skip. Worker selection and skipping rules come from team-state, never from prompts/ directory contents.\n"
|
|
1900
|
+
f"- **Worker Preamble Path (single anchor — replaces inlined `[Required reading]` and `[Error reporting]` blocks).** Every worker prompt you construct MUST include the anchor header `**Worker Preamble Path:** {ctx.get('WORKER_PROMPT_PREAMBLE_PATH', str(Path.home() / '.okstra' / 'templates' / 'worker-prompt-preamble.md'))}`. The file at that path is the canonical SSOT for Required Reading + Error Reporting + Output sections; workers Read it end-to-end before producing output. Do NOT re-inline those blocks into worker prompt bodies — that is the legacy ~80-line dispatch boilerplate that this anchor is designed to replace."
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1776
1903
|
# Compute results (deterministic from ctx, 덮어쓰기)
|
|
1904
|
+
ctx["LEAD_INTRO_LINE"] = lead_intro_line
|
|
1905
|
+
ctx["LEAD_BOOTSTRAP_INSTRUCTION"] = lead_bootstrap_instruction
|
|
1906
|
+
ctx["LEAD_SESSION_BLOCK"] = lead_session_block
|
|
1777
1907
|
ctx["TEAM_CREATION_GATE"] = team_creation_gate_block
|
|
1908
|
+
ctx["WORKER_DISPATCH_GUIDANCE"] = worker_dispatch_guidance
|
|
1778
1909
|
ctx["WORKER_RESULT_PATH_LINES"] = "\n".join(worker_result_lines)
|
|
1779
1910
|
ctx["MODEL_ASSIGNMENT_LINES"] = "\n".join(model_assignment_lines)
|
|
1780
1911
|
ctx["TEAM_ROLE_LINES"] = "\n".join(team_role_lines)
|