okstra 0.34.1 → 0.36.1
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 +27 -19
- package/README.md +27 -19
- package/docs/kr/architecture.md +59 -45
- package/docs/kr/cli.md +61 -18
- package/docs/pr-template-usage.md +65 -0
- package/docs/project-structure-overview.md +353 -354
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
- package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +1700 -0
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
- package/docs/task-process/README.md +74 -0
- package/docs/task-process/common-flow.md +166 -0
- package/docs/task-process/error-analysis.md +101 -0
- package/docs/task-process/final-verification.md +167 -0
- package/docs/task-process/implementation-planning.md +128 -0
- package/docs/task-process/implementation.md +149 -0
- package/docs/task-process/release-handoff.md +206 -0
- package/docs/task-process/requirements-discovery.md +115 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +30 -7
- package/runtime/agents/workers/claude-worker.md +31 -6
- package/runtime/agents/workers/codex-worker.md +37 -10
- package/runtime/agents/workers/gemini-worker.md +34 -7
- package/runtime/agents/workers/report-writer-worker.md +19 -10
- package/runtime/bin/okstra-central.sh +6 -6
- package/runtime/bin/okstra-codex-exec.sh +49 -28
- package/runtime/bin/okstra-gemini-exec.sh +39 -21
- package/runtime/bin/okstra-render-final-report.py +13 -2
- package/runtime/bin/okstra-wrapper-status.py +155 -0
- package/runtime/bin/okstra.sh +2 -2
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/profiles/_common-contract.md +11 -6
- package/runtime/prompts/profiles/_implementation-deliverable.md +53 -0
- package/runtime/prompts/profiles/_implementation-executor.md +60 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +76 -0
- package/runtime/prompts/profiles/error-analysis.md +3 -7
- package/runtime/prompts/profiles/implementation-planning.md +22 -21
- package/runtime/prompts/profiles/implementation.md +28 -118
- package/runtime/prompts/profiles/improvement-discovery.md +42 -0
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -12
- package/runtime/prompts/wizard/prompts.ko.json +230 -0
- package/runtime/python/lib/okstra/cli.sh +2 -49
- package/runtime/python/lib/okstra/globals.sh +21 -21
- package/runtime/python/lib/okstra/interactive.sh +7 -7
- package/runtime/python/okstra_ctl/clarification_items.py +3 -9
- package/runtime/python/okstra_ctl/consumers.py +53 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
- package/runtime/python/okstra_ctl/i18n.py +73 -0
- package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
- package/runtime/python/okstra_ctl/index.py +1 -1
- package/runtime/python/okstra_ctl/paths.py +26 -20
- package/runtime/python/okstra_ctl/render.py +166 -207
- package/runtime/python/okstra_ctl/render_final_report.py +53 -10
- package/runtime/python/okstra_ctl/run.py +299 -108
- package/runtime/python/okstra_ctl/run_context.py +22 -0
- package/runtime/python/okstra_ctl/seeding.py +186 -0
- package/runtime/python/okstra_ctl/session.py +65 -7
- package/runtime/python/okstra_ctl/wizard.py +348 -127
- package/runtime/python/okstra_ctl/workflow.py +21 -2
- package/runtime/python/okstra_ctl/worktree.py +54 -1
- package/runtime/python/okstra_project/resolver.py +4 -3
- package/runtime/python/okstra_token_usage/report.py +2 -2
- package/runtime/schemas/final-report-v1.0.schema.json +22 -16
- package/runtime/skills/okstra-brief/SKILL.md +102 -218
- package/runtime/skills/okstra-convergence/SKILL.md +2 -3
- package/runtime/skills/okstra-inspect/SKILL.md +581 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
- package/runtime/skills/okstra-run/SKILL.md +8 -7
- package/runtime/skills/okstra-schedule/SKILL.md +14 -157
- package/runtime/skills/okstra-setup/SKILL.md +28 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +16 -107
- package/runtime/templates/okstra.CLAUDE.md +104 -0
- package/runtime/templates/reports/brief.template.md +204 -0
- package/runtime/templates/reports/final-report.template.md +93 -98
- package/runtime/templates/reports/i18n/en.json +135 -0
- package/runtime/templates/reports/i18n/ko.json +135 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
- package/runtime/templates/reports/schedule.template.md +12 -3
- package/runtime/templates/reports/task-brief.template.md +2 -2
- package/runtime/templates/worker-prompt-preamble.md +108 -0
- package/runtime/validators/lib/fixtures.sh +30 -0
- package/runtime/validators/lib/runners.sh +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +211 -0
- package/runtime/validators/validate-run.py +121 -26
- package/runtime/validators/validate-workflow.sh +2 -2
- package/runtime/validators/validate_improvement_report.py +275 -0
- package/src/config.mjs +18 -0
- package/src/install.mjs +41 -14
- package/src/setup.mjs +133 -1
- package/src/uninstall.mjs +27 -3
- package/runtime/skills/okstra-history/SKILL.md +0 -165
- package/runtime/skills/okstra-logs/SKILL.md +0 -173
- package/runtime/skills/okstra-report-finder/SKILL.md +0 -111
- package/runtime/skills/okstra-status/SKILL.md +0 -246
- package/runtime/skills/okstra-time-summary/SKILL.md +0 -172
|
@@ -22,6 +22,16 @@ import re
|
|
|
22
22
|
import sys
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
|
+
|
|
26
|
+
class TokenRenderError(Exception):
|
|
27
|
+
"""Raised when a template references a `{{TOKEN}}` not present in ctx.
|
|
28
|
+
|
|
29
|
+
Specific to the pure-token renderer in this module. Distinct from
|
|
30
|
+
`okstra_ctl.render_final_report.FinalReportRenderError` which wraps
|
|
31
|
+
jinja2 / IO failures during final-report rendering.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
25
35
|
# --------------------------------------------------------------------------- #
|
|
26
36
|
# helpers
|
|
27
37
|
# --------------------------------------------------------------------------- #
|
|
@@ -152,9 +162,7 @@ def _frontmatter_mapping(ctx: dict) -> dict:
|
|
|
152
162
|
task_key = (ctx.get("TASK_KEY") or "").strip()
|
|
153
163
|
task_date = (ctx.get("TASK_DATE") or "").strip()
|
|
154
164
|
doc_type = (ctx.get("DOC_TYPE") or "").strip()
|
|
155
|
-
|
|
156
|
-
# `ANALYSIS_TYPE` fallback (workflow 의 render mapping 과 동일 우선순위).
|
|
157
|
-
task_type = (ctx.get("TASK_TYPE") or ctx.get("ANALYSIS_TYPE") or "").strip()
|
|
165
|
+
task_type = (ctx.get("TASK_TYPE") or "").strip()
|
|
158
166
|
|
|
159
167
|
fm_id = _frontmatter_id_from_task_key(task_key)
|
|
160
168
|
fm_id_scalar = f'"{fm_id}"' if fm_id else f'"{_FM_DEFAULT}"'
|
|
@@ -179,7 +187,7 @@ def _frontmatter_mapping(ctx: dict) -> dict:
|
|
|
179
187
|
|
|
180
188
|
def _resolve_workers(ctx: dict) -> list[str]:
|
|
181
189
|
return [
|
|
182
|
-
w.strip() for w in ctx.get("
|
|
190
|
+
w.strip() for w in ctx.get("RECOMMENDED_ANALYSERS", "").split(",") if w.strip()
|
|
183
191
|
]
|
|
184
192
|
|
|
185
193
|
|
|
@@ -190,7 +198,7 @@ def _worker_catalog(ctx: dict) -> dict:
|
|
|
190
198
|
"role": "Claude worker",
|
|
191
199
|
"agent": "claude",
|
|
192
200
|
"agentLabel": "Claude Code",
|
|
193
|
-
"model": ctx.get("
|
|
201
|
+
"model": ctx.get("CLAUDE_WORKER_MODEL", ""),
|
|
194
202
|
"modelExecutionValue": ctx.get("CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""),
|
|
195
203
|
"resultPath": ctx.get("CLAUDE_WORKER_RESULT_RELATIVE_PATH", ""),
|
|
196
204
|
"promptPath": ctx.get("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", ""),
|
|
@@ -200,7 +208,7 @@ def _worker_catalog(ctx: dict) -> dict:
|
|
|
200
208
|
"role": "Codex worker",
|
|
201
209
|
"agent": "codex",
|
|
202
210
|
"agentLabel": "Codex",
|
|
203
|
-
"model": ctx.get("
|
|
211
|
+
"model": ctx.get("CODEX_WORKER_MODEL", ""),
|
|
204
212
|
"modelExecutionValue": ctx.get("CODEX_WORKER_MODEL_EXECUTION_VALUE", ""),
|
|
205
213
|
"resultPath": ctx.get("CODEX_WORKER_RESULT_RELATIVE_PATH", ""),
|
|
206
214
|
"promptPath": ctx.get("CODEX_WORKER_PROMPT_RELATIVE_PATH", ""),
|
|
@@ -210,7 +218,7 @@ def _worker_catalog(ctx: dict) -> dict:
|
|
|
210
218
|
"role": "Gemini worker",
|
|
211
219
|
"agent": "gemini",
|
|
212
220
|
"agentLabel": "Gemini",
|
|
213
|
-
"model": ctx.get("
|
|
221
|
+
"model": ctx.get("GEMINI_WORKER_MODEL", ""),
|
|
214
222
|
"modelExecutionValue": ctx.get("GEMINI_WORKER_MODEL_EXECUTION_VALUE", ""),
|
|
215
223
|
"resultPath": ctx.get("GEMINI_WORKER_RESULT_RELATIVE_PATH", ""),
|
|
216
224
|
"promptPath": ctx.get("GEMINI_WORKER_PROMPT_RELATIVE_PATH", ""),
|
|
@@ -220,7 +228,7 @@ def _worker_catalog(ctx: dict) -> dict:
|
|
|
220
228
|
"role": "Report writer worker",
|
|
221
229
|
"agent": "claude",
|
|
222
230
|
"agentLabel": "Claude Code",
|
|
223
|
-
"model": ctx.get("
|
|
231
|
+
"model": ctx.get("REPORT_WRITER_MODEL", ""),
|
|
224
232
|
"modelExecutionValue": ctx.get("REPORT_WRITER_MODEL_EXECUTION_VALUE", ""),
|
|
225
233
|
"resultPath": ctx.get("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", ""),
|
|
226
234
|
"promptPath": ctx.get("REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH", ""),
|
|
@@ -255,13 +263,13 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
|
|
|
255
263
|
payload = {
|
|
256
264
|
"schemaVersion": "1.0",
|
|
257
265
|
"taskKey": ctx.get("TASK_KEY", ""),
|
|
258
|
-
"taskType": ctx.get("
|
|
266
|
+
"taskType": ctx.get("TASK_TYPE", ""),
|
|
259
267
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
260
268
|
"workflowState": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
261
269
|
"lead": {
|
|
262
270
|
"role": "Claude lead",
|
|
263
271
|
"agent": "claude",
|
|
264
|
-
"model": ctx.get("
|
|
272
|
+
"model": ctx.get("LEAD_MODEL", ""),
|
|
265
273
|
"modelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
|
|
266
274
|
"status": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
267
275
|
"sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
|
|
@@ -311,7 +319,7 @@ def render_reference_expectations(brief_path: str, output_path: str, ctx: dict)
|
|
|
311
319
|
"# Task Reference Expectations",
|
|
312
320
|
"",
|
|
313
321
|
f"- Task Key: `{ctx.get('TASK_KEY', '')}`",
|
|
314
|
-
f"- Task Type: `{ctx.get('
|
|
322
|
+
f"- Task Type: `{ctx.get('TASK_TYPE', '')}`",
|
|
315
323
|
f"- Source brief snapshot: `{brief_relative}`",
|
|
316
324
|
"",
|
|
317
325
|
"## Usage Rules",
|
|
@@ -457,7 +465,7 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
|
|
|
457
465
|
|
|
458
466
|
|
|
459
467
|
def render_latest_task_discovery(output_path: str, ctx: dict) -> None:
|
|
460
|
-
task_manifest_path = Path(ctx.get("
|
|
468
|
+
task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
|
|
461
469
|
task_manifest = {}
|
|
462
470
|
if task_manifest_path.exists():
|
|
463
471
|
try:
|
|
@@ -473,7 +481,7 @@ def render_latest_task_discovery(output_path: str, ctx: dict) -> None:
|
|
|
473
481
|
"schemaVersion": "1.0",
|
|
474
482
|
"updatedAt": ctx.get("RUN_TIMESTAMP_ISO", ""),
|
|
475
483
|
"taskKey": ctx.get("TASK_KEY", ""),
|
|
476
|
-
"taskType": ctx.get("
|
|
484
|
+
"taskType": ctx.get("TASK_TYPE", ""),
|
|
477
485
|
"workCategory": task_manifest.get(
|
|
478
486
|
"workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
|
|
479
487
|
),
|
|
@@ -633,7 +641,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
633
641
|
if isinstance(workflow.get("phaseStates"), dict)
|
|
634
642
|
else {}
|
|
635
643
|
)
|
|
636
|
-
current_phase = ctx.get("WORKFLOW_CURRENT_PHASE", ctx.get("
|
|
644
|
+
current_phase = ctx.get("WORKFLOW_CURRENT_PHASE", ctx.get("TASK_TYPE", ""))
|
|
637
645
|
current_phase_state = ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "not-started")
|
|
638
646
|
for phase in phase_sequence:
|
|
639
647
|
phase_states.setdefault(phase, "not-started")
|
|
@@ -643,15 +651,25 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
643
651
|
"WORKFLOW_WORK_CATEGORY", "unknown"
|
|
644
652
|
)
|
|
645
653
|
# Compute the canonical next phase from current_phase deterministically.
|
|
646
|
-
# Only
|
|
647
|
-
#
|
|
648
|
-
#
|
|
649
|
-
#
|
|
654
|
+
# Only advance past `current_phase` when its state is terminal-success
|
|
655
|
+
# (`completed`). For `prepared` / `in-progress` / `blocked` / `error` /
|
|
656
|
+
# `contract-violated` states the recommendation MUST stay on
|
|
657
|
+
# `current_phase` — advancing prematurely makes wizards (and humans)
|
|
658
|
+
# think the current phase is done when it has merely been provisioned.
|
|
659
|
+
# Historical bug: implementation provisioned in `prepared` state caused
|
|
660
|
+
# the wizard to recommend `final-verification` as the default task_type.
|
|
650
661
|
canonical_next = default_next_phase.get(
|
|
651
662
|
current_phase, ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "unknown")
|
|
652
663
|
)
|
|
653
664
|
existing_next = workflow.get("nextRecommendedPhase") or ""
|
|
654
|
-
|
|
665
|
+
terminal_success_states = {"completed"}
|
|
666
|
+
if current_phase_state not in terminal_success_states:
|
|
667
|
+
# Current phase has not finished — recommend staying on it. Suppress
|
|
668
|
+
# any stale `existing_next` that points further forward in the
|
|
669
|
+
# sequence; only honour a stale value if it points to the SAME
|
|
670
|
+
# current_phase (i.e. encodes "re-enter current phase").
|
|
671
|
+
next_recommended_phase = current_phase
|
|
672
|
+
elif existing_next and existing_next != current_phase:
|
|
655
673
|
next_recommended_phase = existing_next
|
|
656
674
|
else:
|
|
657
675
|
next_recommended_phase = canonical_next
|
|
@@ -746,7 +764,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
746
764
|
"taskGroupPathSegment": ctx.get("TASK_GROUP_SEGMENT", ""),
|
|
747
765
|
"taskIdPathSegment": ctx.get("TASK_ID_SEGMENT", ""),
|
|
748
766
|
"projectRoot": ctx.get("PROJECT_ROOT", ""),
|
|
749
|
-
"taskType": ctx.get("
|
|
767
|
+
"taskType": ctx.get("TASK_TYPE", ""),
|
|
750
768
|
"workCategory": work_category,
|
|
751
769
|
"taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
|
|
752
770
|
"recommendedWorkers": reviewers,
|
|
@@ -807,7 +825,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
807
825
|
"resultContract": {
|
|
808
826
|
"leadAgent": "claude",
|
|
809
827
|
"leadRole": "Claude lead",
|
|
810
|
-
"leadModel": ctx.get("
|
|
828
|
+
"leadModel": ctx.get("LEAD_MODEL", ""),
|
|
811
829
|
"leadModelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
|
|
812
830
|
"leadExecutionMode": "synthesis-only",
|
|
813
831
|
"finalSynthesisOwner": "Claude lead",
|
|
@@ -890,7 +908,7 @@ def _build_convergence_block(ctx: dict) -> dict:
|
|
|
890
908
|
- `OKSTRA_PLAN_VERIFICATION`: "true" | "false" | "" (empty → default True).
|
|
891
909
|
Wired from CLI `--no-plan-verification` (sets "false").
|
|
892
910
|
"""
|
|
893
|
-
task_type = ctx.get("
|
|
911
|
+
task_type = ctx.get("TASK_TYPE", "")
|
|
894
912
|
default_max_rounds = 1 if task_type == "requirements-discovery" else 2
|
|
895
913
|
raw_plan_verify = (ctx.get("OKSTRA_PLAN_VERIFICATION", "") or "").strip().lower()
|
|
896
914
|
plan_verify_enabled = raw_plan_verify != "false"
|
|
@@ -907,7 +925,7 @@ def _build_convergence_block(ctx: dict) -> dict:
|
|
|
907
925
|
|
|
908
926
|
|
|
909
927
|
def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
910
|
-
task_manifest_path = Path(ctx.get("
|
|
928
|
+
task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
|
|
911
929
|
task_manifest = {}
|
|
912
930
|
if task_manifest_path.exists():
|
|
913
931
|
try:
|
|
@@ -931,7 +949,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
931
949
|
"taskGroup": ctx.get("TASK_GROUP", ""),
|
|
932
950
|
"taskId": ctx.get("TASK_ID", ""),
|
|
933
951
|
"taskKey": ctx.get("TASK_KEY", ""),
|
|
934
|
-
"taskType": ctx.get("
|
|
952
|
+
"taskType": ctx.get("TASK_TYPE", ""),
|
|
935
953
|
"workCategory": task_manifest.get(
|
|
936
954
|
"workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
|
|
937
955
|
),
|
|
@@ -995,7 +1013,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
995
1013
|
"teamContract": {
|
|
996
1014
|
"leadAgent": "claude",
|
|
997
1015
|
"leadRole": "Claude lead",
|
|
998
|
-
"leadModel": ctx.get("
|
|
1016
|
+
"leadModel": ctx.get("LEAD_MODEL", ""),
|
|
999
1017
|
"leadModelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
|
|
1000
1018
|
"leadExecutionMode": "synthesis-only",
|
|
1001
1019
|
"finalSynthesisOwner": "Claude lead",
|
|
@@ -1034,7 +1052,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1034
1052
|
|
|
1035
1053
|
|
|
1036
1054
|
def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
1037
|
-
task_manifest_path = Path(ctx.get("
|
|
1055
|
+
task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
|
|
1038
1056
|
task_manifest = {}
|
|
1039
1057
|
if task_manifest_path.exists():
|
|
1040
1058
|
try:
|
|
@@ -1056,7 +1074,7 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
|
1056
1074
|
except Exception:
|
|
1057
1075
|
existing = {}
|
|
1058
1076
|
runs = existing.get("runs", [])
|
|
1059
|
-
current_run_manifest_path = ctx.get("
|
|
1077
|
+
current_run_manifest_path = ctx.get("RUN_MANIFEST_PATH", "")
|
|
1060
1078
|
current_run_manifest_relative_path = ctx.get("RUN_MANIFEST_RELATIVE_PATH", "")
|
|
1061
1079
|
filtered = [
|
|
1062
1080
|
item
|
|
@@ -1085,7 +1103,7 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
|
1085
1103
|
"sessions": ctx.get("RUN_SESSIONS_SEQ", ""),
|
|
1086
1104
|
"workerResults": ctx.get("WORKER_RESULTS_SEQ", ""),
|
|
1087
1105
|
},
|
|
1088
|
-
"taskType": ctx.get("
|
|
1106
|
+
"taskType": ctx.get("TASK_TYPE", ""),
|
|
1089
1107
|
"workCategory": task_manifest.get(
|
|
1090
1108
|
"workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
|
|
1091
1109
|
),
|
|
@@ -1127,7 +1145,7 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
|
1127
1145
|
|
|
1128
1146
|
def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
1129
1147
|
template = Path(template_path).read_text(encoding="utf-8")
|
|
1130
|
-
task_manifest_path = Path(ctx["
|
|
1148
|
+
task_manifest_path = Path(ctx["TASK_MANIFEST_PATH"])
|
|
1131
1149
|
task_manifest = {}
|
|
1132
1150
|
if task_manifest_path.exists():
|
|
1133
1151
|
try:
|
|
@@ -1187,7 +1205,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
1187
1205
|
)
|
|
1188
1206
|
mapping = {
|
|
1189
1207
|
"{{TASK_KEY}}": task_manifest.get("taskKey", ctx.get("TASK_KEY", "")),
|
|
1190
|
-
"{{TASK_TYPE}}": task_manifest.get("taskType", ctx.get("
|
|
1208
|
+
"{{TASK_TYPE}}": task_manifest.get("taskType", ctx.get("TASK_TYPE", "")),
|
|
1191
1209
|
"{{TASK_DATE}}": ctx.get("TASK_DATE", ""),
|
|
1192
1210
|
"{{PROJECT_ID}}": ctx.get("PROJECT_ID", ""),
|
|
1193
1211
|
"{{TASK_GROUP}}": ctx.get("TASK_GROUP", ""),
|
|
@@ -1202,7 +1220,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
1202
1220
|
"{{RECOMMENDED_ANALYSERS}}": ", ".join(
|
|
1203
1221
|
task_manifest.get("recommendedWorkers", [])
|
|
1204
1222
|
),
|
|
1205
|
-
"{{LEAD_MODEL}}": rc.get("leadModel", ctx.get("
|
|
1223
|
+
"{{LEAD_MODEL}}": rc.get("leadModel", ctx.get("LEAD_MODEL", "")),
|
|
1206
1224
|
"{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
|
|
1207
1225
|
"{{LATEST_RUN_RELATIVE_PATH}}": task_manifest.get(
|
|
1208
1226
|
"latestRunPath", ctx.get("LATEST_RUN_RELATIVE_PATH", "")
|
|
@@ -1222,11 +1240,11 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
1222
1240
|
),
|
|
1223
1241
|
"{{MODEL_ASSIGNMENT_LINES}}": "\n".join(
|
|
1224
1242
|
[
|
|
1225
|
-
f"- `Claude lead`: `{rc.get('leadModel', ctx.get('
|
|
1226
|
-
f"- `Claude worker`: `{ctx.get('
|
|
1227
|
-
f"- `Codex worker`: `{ctx.get('
|
|
1228
|
-
f"- `Gemini worker`: `{ctx.get('
|
|
1229
|
-
f"- `Report writer worker`: `{ctx.get('
|
|
1243
|
+
f"- `Claude lead`: `{rc.get('leadModel', ctx.get('LEAD_MODEL', ''))}`",
|
|
1244
|
+
f"- `Claude worker`: `{ctx.get('CLAUDE_WORKER_MODEL', '')}`",
|
|
1245
|
+
f"- `Codex worker`: `{ctx.get('CODEX_WORKER_MODEL', '')}`",
|
|
1246
|
+
f"- `Gemini worker`: `{ctx.get('GEMINI_WORKER_MODEL', '')}`",
|
|
1247
|
+
f"- `Report writer worker`: `{ctx.get('REPORT_WRITER_MODEL', '')}`",
|
|
1230
1248
|
]
|
|
1231
1249
|
),
|
|
1232
1250
|
"{{TASK_MANIFEST_RELATIVE_PATH}}": task_manifest.get(
|
|
@@ -1291,7 +1309,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
1291
1309
|
rendered = template
|
|
1292
1310
|
for k, v in mapping.items():
|
|
1293
1311
|
rendered = rendered.replace(k, v)
|
|
1294
|
-
rendered = _strip_phase_blocks(rendered, ctx.get("
|
|
1312
|
+
rendered = _strip_phase_blocks(rendered, ctx.get("TASK_TYPE", ""))
|
|
1295
1313
|
_write_text(Path(output_path), rendered.rstrip() + "\n")
|
|
1296
1314
|
|
|
1297
1315
|
|
|
@@ -1355,11 +1373,24 @@ def build_available_mcp_servers_block(project_root: Path) -> str:
|
|
|
1355
1373
|
# --------------------------------------------------------------------------- #
|
|
1356
1374
|
|
|
1357
1375
|
|
|
1358
|
-
def
|
|
1359
|
-
|
|
1376
|
+
def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
1377
|
+
"""Populate ctx in-place with the 9 derived lead-prompt tokens.
|
|
1378
|
+
|
|
1379
|
+
Tokens that are not 1:1 with a ctx key (TEAM_CREATION_GATE,
|
|
1380
|
+
WORKER_RESULT_PATH_LINES, TEAM_ROLE_LINES, MODEL_ASSIGNMENT_LINES,
|
|
1381
|
+
REQUIRED_WORKER_ROLE_SENTENCE, GEMINI_ATTEMPT_SENTENCE,
|
|
1382
|
+
PREFERRED_WORKER_RESULTS_SENTENCE, EXECUTION_STATUS_EXACT_ENTRIES,
|
|
1383
|
+
EXECUTION_STATUS_TABLE_ROWS) are computed deterministically from ctx
|
|
1384
|
+
so the pure-lookup renderer (`render_template_with_ctx`) can resolve
|
|
1385
|
+
them via plain `ctx[token]` lookup.
|
|
1386
|
+
|
|
1387
|
+
Always overwrites — caller-supplied values for these 9 keys are replaced
|
|
1388
|
+
on every call. For optional defaults (VALIDATION_STATUS etc.) use the
|
|
1389
|
+
companion `apply_lead_prompt_defaults` which preserves caller values.
|
|
1390
|
+
"""
|
|
1360
1391
|
selected = _resolve_workers(ctx)
|
|
1361
1392
|
catalog = _worker_catalog(ctx)
|
|
1362
|
-
lead_model = ctx.get("
|
|
1393
|
+
lead_model = ctx.get("LEAD_MODEL", "")
|
|
1363
1394
|
lead_model_execution = ctx.get("LEAD_MODEL_EXECUTION_VALUE", "")
|
|
1364
1395
|
|
|
1365
1396
|
def fmt_assignment(role: str, model: str, execution: str) -> str:
|
|
@@ -1367,12 +1398,12 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
|
|
|
1367
1398
|
return f"- `{role}`: `{model}` (launch value: `{execution}`)"
|
|
1368
1399
|
return f"- `{role}`: `{model}`"
|
|
1369
1400
|
|
|
1370
|
-
worker_result_lines = []
|
|
1401
|
+
worker_result_lines: list[str] = []
|
|
1371
1402
|
team_role_lines = [f" 1. `Claude lead` (assigned model: `{lead_model}`)"]
|
|
1372
1403
|
model_assignment_lines = [
|
|
1373
1404
|
fmt_assignment("Claude lead", lead_model, lead_model_execution)
|
|
1374
1405
|
]
|
|
1375
|
-
worker_role_labels = []
|
|
1406
|
+
worker_role_labels: list[str] = []
|
|
1376
1407
|
execution_status_entries = ["`Claude lead`"]
|
|
1377
1408
|
execution_status_table_lines = [
|
|
1378
1409
|
"| 에이전트 | 역할 | 모델 | 상태 | 핵심 발견 요약 |",
|
|
@@ -1424,7 +1455,7 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
|
|
|
1424
1455
|
# selected) is single-lead and MUST NOT call `TeamCreate`. Emit a
|
|
1425
1456
|
# short notice instead of the BLOCKING gate.
|
|
1426
1457
|
# - All other phases keep the full team-creation contract.
|
|
1427
|
-
task_type = ctx.get("
|
|
1458
|
+
task_type = ctx.get("TASK_TYPE", "")
|
|
1428
1459
|
if task_type == "release-handoff" or not selected:
|
|
1429
1460
|
team_creation_gate_block = (
|
|
1430
1461
|
"## Single-Lead Phase (no team creation)\n"
|
|
@@ -1466,172 +1497,97 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
|
|
|
1466
1497
|
"response is to go back to step 2 — NOT to strip `team_name` and retry."
|
|
1467
1498
|
)
|
|
1468
1499
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
"
|
|
1500
|
-
"
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
"
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
"{{CODEX_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get(
|
|
1529
|
-
"CODEX_WORKER_ERRORS_SIDECAR_FILE", ""
|
|
1530
|
-
),
|
|
1531
|
-
"{{CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get(
|
|
1532
|
-
"CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""
|
|
1533
|
-
),
|
|
1534
|
-
"{{GEMINI_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get(
|
|
1535
|
-
"GEMINI_WORKER_ERRORS_SIDECAR_FILE", ""
|
|
1536
|
-
),
|
|
1537
|
-
"{{GEMINI_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get(
|
|
1538
|
-
"GEMINI_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""
|
|
1539
|
-
),
|
|
1540
|
-
"{{REPORT_WRITER_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get(
|
|
1541
|
-
"REPORT_WRITER_WORKER_ERRORS_SIDECAR_FILE", ""
|
|
1542
|
-
),
|
|
1543
|
-
"{{REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get(
|
|
1544
|
-
"REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""
|
|
1545
|
-
),
|
|
1546
|
-
"{{LEAD_MODEL}}": lead_model,
|
|
1547
|
-
"{{LEAD_MODEL_EXECUTION_VALUE}}": lead_model_execution,
|
|
1548
|
-
"{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
|
|
1549
|
-
"{{CLAUDE_WORKER_MODEL}}": ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", ""),
|
|
1550
|
-
"{{CLAUDE_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get(
|
|
1551
|
-
"CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""
|
|
1552
|
-
),
|
|
1553
|
-
"{{CODEX_WORKER_MODEL}}": ctx.get("CODEX_WORKER_MODEL_DISPLAY", ""),
|
|
1554
|
-
"{{CODEX_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get(
|
|
1555
|
-
"CODEX_WORKER_MODEL_EXECUTION_VALUE", ""
|
|
1556
|
-
),
|
|
1557
|
-
"{{GEMINI_WORKER_MODEL}}": ctx.get("GEMINI_WORKER_MODEL_DISPLAY", ""),
|
|
1558
|
-
"{{GEMINI_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get(
|
|
1559
|
-
"GEMINI_WORKER_MODEL_EXECUTION_VALUE", ""
|
|
1560
|
-
),
|
|
1561
|
-
"{{REPORT_WRITER_MODEL}}": ctx.get("REPORT_WRITER_MODEL_DISPLAY", ""),
|
|
1562
|
-
"{{REPORT_WRITER_MODEL_EXECUTION_VALUE}}": ctx.get(
|
|
1563
|
-
"REPORT_WRITER_MODEL_EXECUTION_VALUE", ""
|
|
1564
|
-
),
|
|
1565
|
-
"{{WORKER_RESULT_PATH_LINES}}": "\n".join(worker_result_lines),
|
|
1566
|
-
"{{MODEL_ASSIGNMENT_LINES}}": "\n".join(model_assignment_lines),
|
|
1567
|
-
"{{TEAM_ROLE_LINES}}": "\n".join(team_role_lines),
|
|
1568
|
-
"{{REQUIRED_WORKER_ROLE_SENTENCE}}": worker_role_sentence,
|
|
1569
|
-
"{{GEMINI_ATTEMPT_SENTENCE}}": worker_attempt_sentence,
|
|
1570
|
-
"{{PREFERRED_WORKER_RESULTS_SENTENCE}}": preferred_results_sentence,
|
|
1571
|
-
"{{EXECUTION_STATUS_EXACT_ENTRIES}}": ", ".join(execution_status_entries),
|
|
1572
|
-
"{{EXECUTION_STATUS_TABLE_ROWS}}": "\n".join(execution_status_table_lines),
|
|
1573
|
-
"{{FINAL_REPORT_TEMPLATE_PATH}}": ctx.get("FINAL_REPORT_TEMPLATE_FILE", ""),
|
|
1574
|
-
"{{FINAL_REPORT_TEMPLATE_RELATIVE_PATH}}": ctx.get(
|
|
1575
|
-
"FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""
|
|
1576
|
-
),
|
|
1577
|
-
"{{REFERENCE_EXPECTATIONS_RELATIVE_PATH}}": ctx.get(
|
|
1578
|
-
"REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""
|
|
1579
|
-
),
|
|
1580
|
-
"{{CLAUDE_SESSION_ID}}": ctx.get("CLAUDE_SESSION_ID", ""),
|
|
1581
|
-
"{{CLAUDE_RESUME_COMMAND_PATH}}": ctx.get("CLAUDE_RESUME_COMMAND_FILE", ""),
|
|
1582
|
-
"{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": ctx.get(
|
|
1583
|
-
"CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""
|
|
1584
|
-
),
|
|
1585
|
-
"{{TASK_ROOT_RELATIVE_PATH}}": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
|
|
1586
|
-
"{{TASK_MANIFEST_RELATIVE_PATH}}": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
|
|
1587
|
-
"{{TASK_INDEX_RELATIVE_PATH}}": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
|
|
1588
|
-
"{{INSTRUCTION_SET_RELATIVE_PATH}}": ctx.get(
|
|
1589
|
-
"INSTRUCTION_SET_RELATIVE_PATH", ""
|
|
1590
|
-
),
|
|
1591
|
-
"{{RUNS_RELATIVE_PATH}}": ctx.get("RUNS_RELATIVE_PATH", ""),
|
|
1592
|
-
"{{HISTORY_RELATIVE_PATH}}": ctx.get("HISTORY_RELATIVE_PATH", ""),
|
|
1593
|
-
"{{OKSTRA_DISCOVERY_RELATIVE_PATH}}": ctx.get(
|
|
1594
|
-
"OKSTRA_DISCOVERY_RELATIVE_PATH", ""
|
|
1595
|
-
),
|
|
1596
|
-
"{{OKSTRA_LATEST_TASK_RELATIVE_PATH}}": ctx.get(
|
|
1597
|
-
"OKSTRA_LATEST_TASK_RELATIVE_PATH", ""
|
|
1598
|
-
),
|
|
1599
|
-
"{{OKSTRA_TASK_CATALOG_RELATIVE_PATH}}": ctx.get(
|
|
1600
|
-
"OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""
|
|
1601
|
-
),
|
|
1602
|
-
"{{LATEST_RUN_RELATIVE_PATH}}": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
|
|
1603
|
-
"{{LATEST_REPORT_RELATIVE_PATH}}": ctx.get("LATEST_REPORT_RELATIVE_PATH", ""),
|
|
1604
|
-
"{{TIMELINE_RELATIVE_PATH}}": ctx.get("TIMELINE_RELATIVE_PATH", ""),
|
|
1605
|
-
"{{CURRENT_TASK_STATUS}}": ctx.get("CURRENT_TASK_STATUS", ""),
|
|
1606
|
-
"{{CURRENT_RUN_STATUS}}": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
1607
|
-
"{{VALIDATION_STATUS}}": ctx.get("VALIDATION_STATUS", "not-run"),
|
|
1608
|
-
"{{RELATED_TASKS_BULLETS}}": ctx.get(
|
|
1609
|
-
"RELATED_TASKS_BULLETS", "- None recorded"
|
|
1610
|
-
),
|
|
1611
|
-
"{{RELATED_TASKS_INLINE}}": ctx.get("RELATED_TASKS_INLINE", "None"),
|
|
1612
|
-
"{{WORKFLOW_CURRENT_PHASE}}": ctx.get("WORKFLOW_CURRENT_PHASE", ""),
|
|
1613
|
-
"{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}": ctx.get(
|
|
1614
|
-
"WORKFLOW_NEXT_RECOMMENDED_PHASE", ""
|
|
1615
|
-
),
|
|
1616
|
-
"{{PHASE_ALLOWED_OUTPUTS}}": ctx.get("PHASE_ALLOWED_OUTPUTS", ""),
|
|
1617
|
-
"{{PHASE_FORBIDDEN_ACTIONS}}": ctx.get("PHASE_FORBIDDEN_ACTIONS", ""),
|
|
1618
|
-
"{{AVAILABLE_MCP_SERVERS}}": ctx.get(
|
|
1619
|
-
"AVAILABLE_MCP_SERVERS",
|
|
1620
|
-
build_available_mcp_servers_block(Path(ctx.get("PROJECT_ROOT", "."))),
|
|
1621
|
-
),
|
|
1622
|
-
"{{EXECUTOR_WORKTREE_PATH}}": ctx.get("EXECUTOR_WORKTREE_PATH", ""),
|
|
1623
|
-
"{{EXECUTOR_WORKTREE_BRANCH}}": ctx.get("EXECUTOR_WORKTREE_BRANCH", ""),
|
|
1624
|
-
"{{EXECUTOR_WORKTREE_BASE_REF}}": ctx.get("EXECUTOR_WORKTREE_BASE_REF", ""),
|
|
1625
|
-
"{{EXECUTOR_WORKTREE_STATUS}}": ctx.get("EXECUTOR_WORKTREE_STATUS", ""),
|
|
1626
|
-
"{{EXECUTOR_WORKTREE_NOTE}}": ctx.get("EXECUTOR_WORKTREE_NOTE", ""),
|
|
1627
|
-
}
|
|
1500
|
+
# Compute results (deterministic from ctx, 덮어쓰기)
|
|
1501
|
+
ctx["TEAM_CREATION_GATE"] = team_creation_gate_block
|
|
1502
|
+
ctx["WORKER_RESULT_PATH_LINES"] = "\n".join(worker_result_lines)
|
|
1503
|
+
ctx["MODEL_ASSIGNMENT_LINES"] = "\n".join(model_assignment_lines)
|
|
1504
|
+
ctx["TEAM_ROLE_LINES"] = "\n".join(team_role_lines)
|
|
1505
|
+
ctx["REQUIRED_WORKER_ROLE_SENTENCE"] = worker_role_sentence
|
|
1506
|
+
ctx["GEMINI_ATTEMPT_SENTENCE"] = worker_attempt_sentence
|
|
1507
|
+
ctx["PREFERRED_WORKER_RESULTS_SENTENCE"] = preferred_results_sentence
|
|
1508
|
+
ctx["EXECUTION_STATUS_EXACT_ENTRIES"] = ", ".join(execution_status_entries)
|
|
1509
|
+
ctx["EXECUTION_STATUS_TABLE_ROWS"] = "\n".join(execution_status_table_lines)
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def apply_lead_prompt_defaults(ctx: dict) -> None:
|
|
1513
|
+
"""Apply default values for optional lead-prompt ctx fields.
|
|
1514
|
+
|
|
1515
|
+
Sets four optional tokens that the lead prompt template references but
|
|
1516
|
+
which callers may legitimately leave unset (e.g., no validation has run
|
|
1517
|
+
yet, no related tasks were declared). Caller-supplied values are
|
|
1518
|
+
preserved via `setdefault` / `if-not-in` semantics — this function only
|
|
1519
|
+
fills gaps, never overwrites.
|
|
1520
|
+
|
|
1521
|
+
Companion to `inject_lead_prompt_computed_tokens` (which always
|
|
1522
|
+
overwrites with deterministically-derived values). The two functions
|
|
1523
|
+
are kept separate so each has a single clear responsibility:
|
|
1524
|
+
inject = compute-and-overwrite, apply_defaults = fill-if-missing.
|
|
1525
|
+
"""
|
|
1526
|
+
ctx.setdefault("VALIDATION_STATUS", "not-run")
|
|
1527
|
+
ctx.setdefault("RELATED_TASKS_BULLETS", "- None recorded")
|
|
1528
|
+
ctx.setdefault("RELATED_TASKS_INLINE", "None")
|
|
1529
|
+
ctx.setdefault(
|
|
1530
|
+
"WORKER_PROMPT_PREAMBLE_PATH",
|
|
1531
|
+
str(Path.home() / ".okstra" / "templates" / "worker-prompt-preamble.md"),
|
|
1532
|
+
)
|
|
1533
|
+
if "AVAILABLE_MCP_SERVERS" not in ctx:
|
|
1534
|
+
ctx["AVAILABLE_MCP_SERVERS"] = build_available_mcp_servers_block(
|
|
1535
|
+
Path(ctx.get("PROJECT_ROOT", "."))
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
_TOKEN_RE = re.compile(r"\{\{([A-Z][A-Z0-9_]*)\}\}")
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
def render_template_with_ctx(template_path: str, output_path: str, ctx: dict) -> None:
|
|
1543
|
+
"""Render a `{{TOKEN}}` template with pure ctx[token] lookup.
|
|
1544
|
+
|
|
1545
|
+
- Tokens match regex `_TOKEN_RE` (uppercase snake).
|
|
1546
|
+
- Each token MUST exist in ctx. Missing → `TokenRenderError` (fail-fast).
|
|
1547
|
+
- Phase block stripping (`{% if header.taskType == 'X' %} ... {% endif %}`)
|
|
1548
|
+
is applied per `ctx['TASK_TYPE']`.
|
|
1549
|
+
- Frontmatter mapping (`_frontmatter_mapping`) is overlaid (same as legacy
|
|
1550
|
+
renderer).
|
|
1551
|
+
|
|
1552
|
+
Callers that need computed tokens (team_creation_gate etc.) MUST call
|
|
1553
|
+
`inject_lead_prompt_computed_tokens(ctx)` BEFORE invoking this function.
|
|
1554
|
+
Optional defaults (VALIDATION_STATUS etc.) should be filled by calling
|
|
1555
|
+
`apply_lead_prompt_defaults(ctx)` in the same setup step.
|
|
1556
|
+
"""
|
|
1557
|
+
template = Path(template_path).read_text(encoding="utf-8")
|
|
1558
|
+
|
|
1628
1559
|
fm_ctx = dict(ctx)
|
|
1629
1560
|
fm_ctx.setdefault("DOC_TYPE", _doc_type_from_template_path(template_path))
|
|
1630
|
-
|
|
1561
|
+
fm_overlay = _frontmatter_mapping(fm_ctx) # {"{{DOC_TITLE}}": "...", ...}
|
|
1562
|
+
|
|
1563
|
+
# frontmatter overlay 가 채우는 키들도 lookup 대상 — 단일 lookup 으로 통일
|
|
1564
|
+
lookup: dict[str, str] = {}
|
|
1565
|
+
for tok_with_braces, value in fm_overlay.items():
|
|
1566
|
+
key = tok_with_braces[2:-2] # "{{X}}" -> "X"
|
|
1567
|
+
lookup[key] = value
|
|
1568
|
+
|
|
1631
1569
|
rendered = template
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1570
|
+
missing: list[str] = []
|
|
1571
|
+
for match in _TOKEN_RE.finditer(template):
|
|
1572
|
+
token = match.group(1)
|
|
1573
|
+
if token in lookup:
|
|
1574
|
+
value = lookup[token]
|
|
1575
|
+
elif token in ctx:
|
|
1576
|
+
value = str(ctx[token])
|
|
1577
|
+
else:
|
|
1578
|
+
missing.append(token)
|
|
1579
|
+
continue
|
|
1580
|
+
rendered = rendered.replace("{{" + token + "}}", value)
|
|
1581
|
+
|
|
1582
|
+
if missing:
|
|
1583
|
+
names = ", ".join(sorted(set(missing)))
|
|
1584
|
+
raise TokenRenderError(
|
|
1585
|
+
f"undefined lead-prompt token(s): {names} (template={template_path}). "
|
|
1586
|
+
f"Add the key(s) to ctx in run.py / "
|
|
1587
|
+
f"inject_lead_prompt_computed_tokens() / apply_lead_prompt_defaults()."
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
rendered = _strip_phase_blocks(rendered, ctx.get("TASK_TYPE", ""))
|
|
1635
1591
|
_write_text(Path(output_path), rendered.rstrip() + "\n")
|
|
1636
1592
|
|
|
1637
1593
|
|
|
@@ -1681,7 +1637,10 @@ def main(argv: list[str]) -> int:
|
|
|
1681
1637
|
render_task_index(template_path, output_path, _load_ctx(ctx_path))
|
|
1682
1638
|
elif sub == "template":
|
|
1683
1639
|
ctx_path, template_path, output_path = rest
|
|
1684
|
-
|
|
1640
|
+
ctx = _load_ctx(ctx_path)
|
|
1641
|
+
inject_lead_prompt_computed_tokens(ctx)
|
|
1642
|
+
apply_lead_prompt_defaults(ctx)
|
|
1643
|
+
render_template_with_ctx(template_path, output_path, ctx)
|
|
1685
1644
|
else:
|
|
1686
1645
|
print(f"unknown subcommand: {sub}", file=sys.stderr)
|
|
1687
1646
|
return 2
|