okstra 0.32.1 → 0.34.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.
@@ -14,13 +14,14 @@ ctx dict 의 schema 는 `okstra_ctl.paths.compute_run_paths()` 의 반환값을
14
14
  기본으로, 호출자가 추가 키 (workflow state / model display / related tasks /
15
15
  session id 등) 를 덧붙여 전달한다.
16
16
  """
17
+
17
18
  from __future__ import annotations
18
19
 
19
20
  import json
21
+ import re
20
22
  import sys
21
23
  from pathlib import Path
22
24
 
23
-
24
25
  # --------------------------------------------------------------------------- #
25
26
  # helpers
26
27
  # --------------------------------------------------------------------------- #
@@ -47,9 +48,47 @@ def _write_json(path: Path, payload: dict) -> None:
47
48
  _write_text(path, json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
48
49
 
49
50
 
51
+ _PHASE_BLOCK_RE = re.compile(
52
+ r"\{% if header\.taskType == '(implementation-planning|release-handoff|implementation|final-verification)' %\}\n(.*?)\{% endif %\}\n",
53
+ re.DOTALL,
54
+ )
55
+
56
+
57
+ def _strip_phase_blocks(text: str, current_phase: str) -> str:
58
+ """Resolve phase-conditional blocks (`{% if header.taskType == 'X' %}
59
+ ... {% endif %}`) against *current_phase*.
60
+
61
+ Blocks whose target equals *current_phase* keep their body (jinja
62
+ markers dropped); blocks targeting a different phase are removed
63
+ entirely. When *current_phase* is empty or not one of the four
64
+ block-targetable phases (e.g. `requirements-discovery`,
65
+ `error-analysis`), every block is dropped — correct because none of
66
+ the `## 4.5` / `4.6` / `4.7` / `4.8` deliverable sections apply
67
+ there.
68
+
69
+ Observed (fontsninja-classifier-v2 RD run): the raw final-report
70
+ template copied into instruction-set/final-report-template.md was
71
+ 43 KB / 631 lines; ~30 KB / ~330 lines belonged to the four other
72
+ phases' deliverables and was never relevant to that run. Stripping
73
+ at copy time cuts the lead/report-writer's baseline by ~7 K tokens
74
+ per phase entry.
75
+
76
+ Inline conditionals (those that begin and end on the same line) are
77
+ intentionally untouched — the regex only matches block-form
78
+ `{% if ... %}\\n ... \\n{% endif %}\\n`.
79
+ """
80
+
81
+ def repl(m: "re.Match[str]") -> str:
82
+ target_phase = m.group(1)
83
+ body = m.group(2)
84
+ return body if target_phase == current_phase else ""
85
+
86
+ return _PHASE_BLOCK_RE.sub(repl, text)
87
+
88
+
50
89
  _FM_DEFAULT = "no-classification"
51
90
 
52
- _FM_TAGS_BASE = ["obsidian", "okstra"]
91
+ _FM_TAGS_BASE = []
53
92
 
54
93
  _FM_TAGS_CATALOG: dict[str, list[str]] = {
55
94
  "task-brief": ["task-brief"],
@@ -139,7 +178,9 @@ def _frontmatter_mapping(ctx: dict) -> dict:
139
178
 
140
179
 
141
180
  def _resolve_workers(ctx: dict) -> list[str]:
142
- return [w.strip() for w in ctx.get("SELECTED_REVIEWERS", "").split(",") if w.strip()]
181
+ return [
182
+ w.strip() for w in ctx.get("SELECTED_REVIEWERS", "").split(",") if w.strip()
183
+ ]
143
184
 
144
185
 
145
186
  def _worker_catalog(ctx: dict) -> dict:
@@ -198,17 +239,19 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
198
239
  workers = []
199
240
  for w in selected:
200
241
  m = catalog[w]
201
- workers.append({
202
- "workerId": m["workerId"],
203
- "role": m["role"],
204
- "agent": m["agent"],
205
- "model": m["model"],
206
- "modelExecutionValue": m["modelExecutionValue"],
207
- "status": "not-run",
208
- "resultPath": m["resultPath"],
209
- "promptPath": m["promptPath"],
210
- "reason": "",
211
- })
242
+ workers.append(
243
+ {
244
+ "workerId": m["workerId"],
245
+ "role": m["role"],
246
+ "agent": m["agent"],
247
+ "model": m["model"],
248
+ "modelExecutionValue": m["modelExecutionValue"],
249
+ "status": "not-run",
250
+ "resultPath": m["resultPath"],
251
+ "promptPath": m["promptPath"],
252
+ "reason": "",
253
+ }
254
+ )
212
255
  payload = {
213
256
  "schemaVersion": "1.0",
214
257
  "taskKey": ctx.get("TASK_KEY", ""),
@@ -280,9 +323,15 @@ def render_reference_expectations(brief_path: str, output_path: str, ctx: dict)
280
323
  "## Configuration References and Expected Values",
281
324
  "",
282
325
  ]
283
- parts.append(config_text or "- No explicit configuration-file expectations were provided in the task brief.")
326
+ parts.append(
327
+ config_text
328
+ or "- No explicit configuration-file expectations were provided in the task brief."
329
+ )
284
330
  parts.extend(["", "## Deployment Manifests and Expected Values", ""])
285
- parts.append(deployment_text or "- No explicit deployment-manifest expectations were provided in the task brief.")
331
+ parts.append(
332
+ deployment_text
333
+ or "- No explicit deployment-manifest expectations were provided in the task brief."
334
+ )
286
335
  _write_text(Path(output_path), "\n".join(parts).rstrip() + "\n")
287
336
 
288
337
 
@@ -315,7 +364,11 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
315
364
  continue
316
365
  task_root = manifest_path.parent
317
366
  timeline_relative = s(manifest, "historyTimelinePath").strip()
318
- timeline_path = (project_root / timeline_relative) if timeline_relative else (task_root / "history" / "timeline.json")
367
+ timeline_path = (
368
+ (project_root / timeline_relative)
369
+ if timeline_relative
370
+ else (task_root / "history" / "timeline.json")
371
+ )
319
372
  latest_run = {}
320
373
  if timeline_path.is_file():
321
374
  try:
@@ -328,43 +381,69 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
328
381
  if isinstance(item, dict):
329
382
  latest_run = item
330
383
  break
331
- workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
332
- entries.append({
333
- "taskKey": task_key,
334
- "taskGroup": s(manifest, "taskGroup"),
335
- "taskId": s(manifest, "taskId"),
336
- "taskGroupPathSegment": s(manifest, "taskGroupPathSegment"),
337
- "taskIdPathSegment": s(manifest, "taskIdPathSegment"),
338
- "taskType": s(manifest, "taskType"),
339
- "workCategory": s(manifest, "workCategory"),
340
- "currentStatus": s(manifest, "currentStatus"),
341
- "workStatus": s(manifest, "workStatus"),
342
- "workStatusUpdatedAt": s(manifest, "workStatusUpdatedAt"),
343
- "workStatusNote": s(manifest, "workStatusNote"),
344
- "updatedAt": s(manifest, "updatedAt"),
345
- "currentPhase": (workflow or {}).get("currentPhase", "") if isinstance(workflow, dict) else "",
346
- "currentPhaseState": (workflow or {}).get("currentPhaseState", "") if isinstance(workflow, dict) else "",
347
- "lastCompletedPhase": (workflow or {}).get("lastCompletedPhase", "") if isinstance(workflow, dict) else "",
348
- "nextRecommendedPhase": (workflow or {}).get("nextRecommendedPhase", "") if isinstance(workflow, dict) else "",
349
- "awaitingApproval": (workflow or {}).get("awaitingApproval", False) if isinstance(workflow, dict) else False,
350
- "routingStatus": (workflow or {}).get("routingStatus", "") if isinstance(workflow, dict) else "",
351
- "taskRootPath": s(manifest, "taskRootPath") or rel(task_root),
352
- "taskManifestPath": s(manifest, "taskManifestPath") or rel(manifest_path),
353
- "taskIndexPath": s(manifest, "taskIndexPath"),
354
- "instructionSetPath": s(manifest, "instructionSetPath"),
355
- "referenceExpectationsPath": s(manifest, "referenceExpectationsPath"),
356
- "taskBriefPath": s(manifest, "taskBriefPath"),
357
- "latestRunPath": s(manifest, "latestRunPath") or s(latest_run, "runDirectoryPath"),
358
- "latestRunManifestPath": s(latest_run, "runManifestPath"),
359
- "latestRunPromptsPath": s(manifest, "latestRunPromptsPath") or s(latest_run, "workerPromptDirectoryPath"),
360
- "latestPromptSnapshotPath": s(latest_run, "promptSnapshotPath"),
361
- "latestTeamStatePath": s(latest_run, "teamStatePath"),
362
- "latestRunStatus": s(manifest, "latestRunStatus") or s(latest_run, "status"),
363
- "latestReportPath": s(manifest, "latestReportPath") or s(latest_run, "reportPath"),
364
- "latestResumeCommandPath": s(manifest, "latestResumeCommandPath") or s(latest_run, "resumeCommandPath"),
365
- "historyTimelinePath": timeline_relative or rel(timeline_path),
366
- })
367
- entries.sort(key=lambda x: (x.get("updatedAt", ""), x.get("taskKey", "")), reverse=True)
384
+ workflow = (
385
+ manifest.get("workflow")
386
+ if isinstance(manifest.get("workflow"), dict)
387
+ else {}
388
+ )
389
+ entries.append(
390
+ {
391
+ "taskKey": task_key,
392
+ "taskGroup": s(manifest, "taskGroup"),
393
+ "taskId": s(manifest, "taskId"),
394
+ "taskGroupPathSegment": s(manifest, "taskGroupPathSegment"),
395
+ "taskIdPathSegment": s(manifest, "taskIdPathSegment"),
396
+ "taskType": s(manifest, "taskType"),
397
+ "workCategory": s(manifest, "workCategory"),
398
+ "currentStatus": s(manifest, "currentStatus"),
399
+ "workStatus": s(manifest, "workStatus"),
400
+ "workStatusUpdatedAt": s(manifest, "workStatusUpdatedAt"),
401
+ "workStatusNote": s(manifest, "workStatusNote"),
402
+ "updatedAt": s(manifest, "updatedAt"),
403
+ "currentPhase": (workflow or {}).get("currentPhase", "")
404
+ if isinstance(workflow, dict)
405
+ else "",
406
+ "currentPhaseState": (workflow or {}).get("currentPhaseState", "")
407
+ if isinstance(workflow, dict)
408
+ else "",
409
+ "lastCompletedPhase": (workflow or {}).get("lastCompletedPhase", "")
410
+ if isinstance(workflow, dict)
411
+ else "",
412
+ "nextRecommendedPhase": (workflow or {}).get("nextRecommendedPhase", "")
413
+ if isinstance(workflow, dict)
414
+ else "",
415
+ "awaitingApproval": (workflow or {}).get("awaitingApproval", False)
416
+ if isinstance(workflow, dict)
417
+ else False,
418
+ "routingStatus": (workflow or {}).get("routingStatus", "")
419
+ if isinstance(workflow, dict)
420
+ else "",
421
+ "taskRootPath": s(manifest, "taskRootPath") or rel(task_root),
422
+ "taskManifestPath": s(manifest, "taskManifestPath")
423
+ or rel(manifest_path),
424
+ "taskIndexPath": s(manifest, "taskIndexPath"),
425
+ "instructionSetPath": s(manifest, "instructionSetPath"),
426
+ "referenceExpectationsPath": s(manifest, "referenceExpectationsPath"),
427
+ "taskBriefPath": s(manifest, "taskBriefPath"),
428
+ "latestRunPath": s(manifest, "latestRunPath")
429
+ or s(latest_run, "runDirectoryPath"),
430
+ "latestRunManifestPath": s(latest_run, "runManifestPath"),
431
+ "latestRunPromptsPath": s(manifest, "latestRunPromptsPath")
432
+ or s(latest_run, "workerPromptDirectoryPath"),
433
+ "latestPromptSnapshotPath": s(latest_run, "promptSnapshotPath"),
434
+ "latestTeamStatePath": s(latest_run, "teamStatePath"),
435
+ "latestRunStatus": s(manifest, "latestRunStatus")
436
+ or s(latest_run, "status"),
437
+ "latestReportPath": s(manifest, "latestReportPath")
438
+ or s(latest_run, "reportPath"),
439
+ "latestResumeCommandPath": s(manifest, "latestResumeCommandPath")
440
+ or s(latest_run, "resumeCommandPath"),
441
+ "historyTimelinePath": timeline_relative or rel(timeline_path),
442
+ }
443
+ )
444
+ entries.sort(
445
+ key=lambda x: (x.get("updatedAt", ""), x.get("taskKey", "")), reverse=True
446
+ )
368
447
  payload = {
369
448
  "schemaVersion": "1.0",
370
449
  "projectId": ctx.get("PROJECT_ID", ""),
@@ -385,22 +464,34 @@ def render_latest_task_discovery(output_path: str, ctx: dict) -> None:
385
464
  task_manifest = json.loads(task_manifest_path.read_text(encoding="utf-8"))
386
465
  except Exception:
387
466
  task_manifest = {}
388
- workflow = task_manifest.get("workflow") if isinstance(task_manifest.get("workflow"), dict) else {}
467
+ workflow = (
468
+ task_manifest.get("workflow")
469
+ if isinstance(task_manifest.get("workflow"), dict)
470
+ else {}
471
+ )
389
472
  payload = {
390
473
  "schemaVersion": "1.0",
391
474
  "updatedAt": ctx.get("RUN_TIMESTAMP_ISO", ""),
392
475
  "taskKey": ctx.get("TASK_KEY", ""),
393
476
  "taskType": ctx.get("ANALYSIS_TYPE", ""),
394
- "workCategory": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
395
- "currentStatus": task_manifest.get("currentStatus", ctx.get("CURRENT_TASK_STATUS", "")),
396
- "latestRunStatus": task_manifest.get("latestRunStatus", ctx.get("CURRENT_RUN_STATUS", "")),
477
+ "workCategory": task_manifest.get(
478
+ "workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
479
+ ),
480
+ "currentStatus": task_manifest.get(
481
+ "currentStatus", ctx.get("CURRENT_TASK_STATUS", "")
482
+ ),
483
+ "latestRunStatus": task_manifest.get(
484
+ "latestRunStatus", ctx.get("CURRENT_RUN_STATUS", "")
485
+ ),
397
486
  "workflow": workflow,
398
487
  "taskRootPath": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
399
488
  "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
400
489
  "taskIndexPath": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
401
490
  "instructionSetPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", ""),
402
491
  "taskCatalogPath": ctx.get("OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""),
403
- "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
492
+ "referenceExpectationsPath": ctx.get(
493
+ "REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""
494
+ ),
404
495
  "latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
405
496
  "latestRunManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
406
497
  "latestRunPromptsPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
@@ -508,8 +599,12 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
508
599
  reviewers = _resolve_workers(ctx)
509
600
  catalog = _worker_catalog(ctx)
510
601
  phase_sequence = [
511
- "requirements-discovery", "error-analysis", "implementation-planning",
512
- "implementation", "final-verification", "release-handoff",
602
+ "requirements-discovery",
603
+ "error-analysis",
604
+ "implementation-planning",
605
+ "implementation",
606
+ "final-verification",
607
+ "release-handoff",
513
608
  ]
514
609
  default_next_phase = {
515
610
  "requirements-discovery": "pending-routing-decision",
@@ -521,18 +616,32 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
521
616
  }
522
617
  required_worker_roles = _required_worker_roles(ctx, reviewers)
523
618
  worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
524
- required_agent_status_entries = ["Claude lead"] + [catalog[item]["role"] for item in reviewers]
619
+ required_agent_status_entries = ["Claude lead"] + [
620
+ catalog[item]["role"] for item in reviewers
621
+ ]
525
622
  related_tasks = json.loads(ctx.get("RELATED_TASKS_JSON", "[]"))
526
- current_report_relative = ctx.get("LATEST_REPORT_RELATIVE_PATH") or ctx.get("FINAL_REPORT_RELATIVE_PATH", "")
527
- workflow = existing.get("workflow", {}) if isinstance(existing.get("workflow"), dict) else {}
528
- phase_states = workflow.get("phaseStates", {}) if isinstance(workflow.get("phaseStates"), dict) else {}
623
+ current_report_relative = ctx.get("LATEST_REPORT_RELATIVE_PATH") or ctx.get(
624
+ "FINAL_REPORT_RELATIVE_PATH", ""
625
+ )
626
+ workflow = (
627
+ existing.get("workflow", {})
628
+ if isinstance(existing.get("workflow"), dict)
629
+ else {}
630
+ )
631
+ phase_states = (
632
+ workflow.get("phaseStates", {})
633
+ if isinstance(workflow.get("phaseStates"), dict)
634
+ else {}
635
+ )
529
636
  current_phase = ctx.get("WORKFLOW_CURRENT_PHASE", ctx.get("ANALYSIS_TYPE", ""))
530
637
  current_phase_state = ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "not-started")
531
638
  for phase in phase_sequence:
532
639
  phase_states.setdefault(phase, "not-started")
533
640
  if current_phase:
534
641
  phase_states[current_phase] = current_phase_state
535
- work_category = existing.get("workCategory") or ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
642
+ work_category = existing.get("workCategory") or ctx.get(
643
+ "WORKFLOW_WORK_CATEGORY", "unknown"
644
+ )
536
645
  # Compute the canonical next phase from current_phase deterministically.
537
646
  # Only preserve `existing.workflow.nextRecommendedPhase` when it is a
538
647
  # legitimate forward pointer — i.e. NOT equal to `current_phase` itself
@@ -546,31 +655,63 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
546
655
  next_recommended_phase = existing_next
547
656
  else:
548
657
  next_recommended_phase = canonical_next
549
- last_completed_phase = workflow.get("lastCompletedPhase") or ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")
550
- routing_status = workflow.get("routingStatus") or ctx.get("WORKFLOW_ROUTING_STATUS", "not-applicable")
658
+ last_completed_phase = workflow.get("lastCompletedPhase") or ctx.get(
659
+ "WORKFLOW_LAST_COMPLETED_PHASE", ""
660
+ )
661
+ routing_status = workflow.get("routingStatus") or ctx.get(
662
+ "WORKFLOW_ROUTING_STATUS", "not-applicable"
663
+ )
551
664
  awaiting_approval = workflow.get("awaitingApproval")
552
665
  if not isinstance(awaiting_approval, bool):
553
666
  awaiting_approval = ctx.get("WORKFLOW_AWAITING_APPROVAL", "false") == "true"
554
667
  render_only = ctx.get("RENDER_ONLY", "") == "true"
555
- existing_checkpoint = existing.get("workflow", {}).get("lastSafeCheckpoint", {}) if isinstance(existing.get("workflow"), dict) else {}
668
+ existing_checkpoint = (
669
+ existing.get("workflow", {}).get("lastSafeCheckpoint", {})
670
+ if isinstance(existing.get("workflow"), dict)
671
+ else {}
672
+ )
556
673
  if not isinstance(existing_checkpoint, dict):
557
674
  existing_checkpoint = {}
558
675
  if render_only:
559
676
  last_safe_checkpoint = {
560
677
  "label": existing_checkpoint.get("label", ""),
561
- "taskManifestPath": existing_checkpoint.get("taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")),
562
- "taskIndexPath": existing_checkpoint.get("taskIndexPath", ctx.get("TASK_INDEX_RELATIVE_PATH", "")),
563
- "latestRunPath": existing_checkpoint.get("latestRunPath", existing.get("latestRunPath", "")),
564
- "latestRunManifestPath": existing_checkpoint.get("latestRunManifestPath", ""),
565
- "latestTeamStatePath": existing_checkpoint.get("latestTeamStatePath", existing.get("teamStatePath", "")),
566
- "latestReportPath": existing_checkpoint.get("latestReportPath", existing.get("latestReportPath", "")),
567
- "latestResumeCommandPath": existing_checkpoint.get("latestResumeCommandPath", existing.get("latestResumeCommandPath", "")),
678
+ "taskManifestPath": existing_checkpoint.get(
679
+ "taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")
680
+ ),
681
+ "taskIndexPath": existing_checkpoint.get(
682
+ "taskIndexPath", ctx.get("TASK_INDEX_RELATIVE_PATH", "")
683
+ ),
684
+ "latestRunPath": existing_checkpoint.get(
685
+ "latestRunPath", existing.get("latestRunPath", "")
686
+ ),
687
+ "latestRunManifestPath": existing_checkpoint.get(
688
+ "latestRunManifestPath", ""
689
+ ),
690
+ "latestTeamStatePath": existing_checkpoint.get(
691
+ "latestTeamStatePath", existing.get("teamStatePath", "")
692
+ ),
693
+ "latestReportPath": existing_checkpoint.get(
694
+ "latestReportPath", existing.get("latestReportPath", "")
695
+ ),
696
+ "latestResumeCommandPath": existing_checkpoint.get(
697
+ "latestResumeCommandPath", existing.get("latestResumeCommandPath", "")
698
+ ),
568
699
  }
569
- latest_run_relative = existing.get("latestRunPath", "") or ctx.get("LATEST_RUN_RELATIVE_PATH", "")
570
- latest_run_status = existing.get("latestRunStatus", "") or ctx.get("CURRENT_RUN_STATUS", "")
571
- latest_run_prompts_relative = existing.get("latestRunPromptsPath", "") or ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
572
- latest_report_relative = existing.get("latestReportPath", "") or current_report_relative
573
- latest_team_state_relative = existing.get("teamStatePath", "") or ctx.get("TEAM_STATE_RELATIVE_PATH", "")
700
+ latest_run_relative = existing.get("latestRunPath", "") or ctx.get(
701
+ "LATEST_RUN_RELATIVE_PATH", ""
702
+ )
703
+ latest_run_status = existing.get("latestRunStatus", "") or ctx.get(
704
+ "CURRENT_RUN_STATUS", ""
705
+ )
706
+ latest_run_prompts_relative = existing.get(
707
+ "latestRunPromptsPath", ""
708
+ ) or ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
709
+ latest_report_relative = (
710
+ existing.get("latestReportPath", "") or current_report_relative
711
+ )
712
+ latest_team_state_relative = existing.get("teamStatePath", "") or ctx.get(
713
+ "TEAM_STATE_RELATIVE_PATH", ""
714
+ )
574
715
  latest_resume_command_relative = existing.get("latestResumeCommandPath", "")
575
716
  else:
576
717
  last_safe_checkpoint = {
@@ -581,14 +722,20 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
581
722
  "latestRunManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
582
723
  "latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
583
724
  "latestReportPath": current_report_relative,
584
- "latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
725
+ "latestResumeCommandPath": ctx.get(
726
+ "CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""
727
+ ),
585
728
  }
586
729
  latest_run_relative = ctx.get("LATEST_RUN_RELATIVE_PATH", "")
587
730
  latest_run_status = ctx.get("CURRENT_RUN_STATUS", "")
588
731
  latest_run_prompts_relative = ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
589
- latest_report_relative = current_report_relative or existing.get("latestReportPath", "")
732
+ latest_report_relative = current_report_relative or existing.get(
733
+ "latestReportPath", ""
734
+ )
590
735
  latest_team_state_relative = ctx.get("TEAM_STATE_RELATIVE_PATH", "")
591
- latest_resume_command_relative = ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "") or existing.get("latestResumeCommandPath", "")
736
+ latest_resume_command_relative = ctx.get(
737
+ "CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""
738
+ ) or existing.get("latestResumeCommandPath", "")
592
739
  convergence_block = _build_convergence_block(ctx)
593
740
  payload = {
594
741
  "schemaVersion": "1.0",
@@ -614,7 +761,9 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
614
761
  "historyPath": ctx.get("HISTORY_RELATIVE_PATH", ""),
615
762
  "historyTimelinePath": ctx.get("TIMELINE_RELATIVE_PATH", ""),
616
763
  "taskCatalogPath": ctx.get("OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""),
617
- "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
764
+ "referenceExpectationsPath": ctx.get(
765
+ "REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""
766
+ ),
618
767
  "latestRunPath": latest_run_relative,
619
768
  "latestRunStatus": latest_run_status,
620
769
  "latestRunPromptsPath": latest_run_prompts_relative,
@@ -633,15 +782,23 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
633
782
  "lastSafeCheckpoint": last_safe_checkpoint,
634
783
  },
635
784
  "artifacts": {
636
- "analysisProfilePath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/analysis-profile.md",
637
- "analysisMaterialPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/analysis-material.md",
638
- "taskBriefCopyPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/task-brief.md",
639
- "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
640
- "claudeExecutionPromptPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "") + "/claude-execution-prompt.md",
785
+ "analysisProfilePath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
786
+ + "/analysis-profile.md",
787
+ "analysisMaterialPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
788
+ + "/analysis-material.md",
789
+ "taskBriefCopyPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
790
+ + "/task-brief.md",
791
+ "referenceExpectationsPath": ctx.get(
792
+ "REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""
793
+ ),
794
+ "claudeExecutionPromptPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
795
+ + "/claude-execution-prompt.md",
641
796
  "leadPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
642
797
  "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
643
798
  "workerPromptPathByWorkerId": worker_prompt_paths,
644
- "finalReportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
799
+ "finalReportTemplatePath": ctx.get(
800
+ "FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""
801
+ ),
645
802
  "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
646
803
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
647
804
  "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
@@ -670,20 +827,33 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
670
827
  "disallowLeadSoloAnalysisAsWorkerResult": True,
671
828
  "disallowGenericParallelOnlyExecution": True,
672
829
  "workerOutputSections": [
673
- "Findings", "Missing Information or Assumptions",
674
- "Safe or Reasonable Areas", "Uncertain Points",
830
+ "Findings",
831
+ "Missing Information or Assumptions",
832
+ "Safe or Reasonable Areas",
833
+ "Uncertain Points",
675
834
  "Recommended Next Actions",
676
835
  ],
677
836
  "finalReportSections": [
678
- "Problem or Validation Summary", "Agent Execution Status",
679
- "Cross Verification Result", "Final Verdict",
837
+ "Problem or Validation Summary",
838
+ "Agent Execution Status",
839
+ "Cross Verification Result",
840
+ "Final Verdict",
680
841
  "Evidence and Detailed Analysis",
681
- "Missing Information and Risk", "Recommended Next Actions",
842
+ "Missing Information and Risk",
843
+ "Recommended Next Actions",
682
844
  ],
683
845
  "statusLabels": [
684
- "prepared", "team-created", "workers-dispatched",
685
- "worker-results-collected", "synthesis-written", "in-progress",
686
- "completed", "contract-violated", "timeout", "error", "not-run",
846
+ "prepared",
847
+ "team-created",
848
+ "workers-dispatched",
849
+ "worker-results-collected",
850
+ "synthesis-written",
851
+ "in-progress",
852
+ "completed",
853
+ "contract-violated",
854
+ "timeout",
855
+ "error",
856
+ "not-run",
687
857
  ],
688
858
  },
689
859
  "contractValidation": {
@@ -749,7 +919,11 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
749
919
  required_worker_roles = _required_worker_roles(ctx, reviewers)
750
920
  worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
751
921
  related_tasks = json.loads(ctx.get("RELATED_TASKS_JSON", "[]"))
752
- workflow = task_manifest.get("workflow", {}) if isinstance(task_manifest.get("workflow"), dict) else {}
922
+ workflow = (
923
+ task_manifest.get("workflow", {})
924
+ if isinstance(task_manifest.get("workflow"), dict)
925
+ else {}
926
+ )
753
927
  payload = {
754
928
  "schemaVersion": "1.0",
755
929
  "okstraVersion": ctx.get("OKSTRA_VERSION", ""),
@@ -758,7 +932,9 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
758
932
  "taskId": ctx.get("TASK_ID", ""),
759
933
  "taskKey": ctx.get("TASK_KEY", ""),
760
934
  "taskType": ctx.get("ANALYSIS_TYPE", ""),
761
- "workCategory": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
935
+ "workCategory": task_manifest.get(
936
+ "workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
937
+ ),
762
938
  "taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
763
939
  "relatedTasks": related_tasks,
764
940
  "recommendedWorkers": reviewers,
@@ -766,7 +942,9 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
766
942
  "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
767
943
  "instructionSetPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", ""),
768
944
  "taskCatalogPath": ctx.get("OKSTRA_TASK_CATALOG_RELATIVE_PATH", ""),
769
- "referenceExpectationsPath": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
945
+ "referenceExpectationsPath": ctx.get(
946
+ "REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""
947
+ ),
770
948
  "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
771
949
  "runDateTimeSegment": ctx.get("RUN_DATETIME_SEGMENT", ""),
772
950
  "runSequencesByCategory": {
@@ -792,13 +970,26 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
792
970
  "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
793
971
  "workflowSnapshot": {
794
972
  "phaseSequence": workflow.get("phaseSequence", []),
795
- "currentPhase": workflow.get("currentPhase", ctx.get("WORKFLOW_CURRENT_PHASE", "")),
796
- "currentPhaseState": workflow.get("currentPhaseState", ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "")),
973
+ "currentPhase": workflow.get(
974
+ "currentPhase", ctx.get("WORKFLOW_CURRENT_PHASE", "")
975
+ ),
976
+ "currentPhaseState": workflow.get(
977
+ "currentPhaseState", ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "")
978
+ ),
797
979
  "phaseStates": workflow.get("phaseStates", {}),
798
- "lastCompletedPhase": workflow.get("lastCompletedPhase", ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")),
799
- "nextRecommendedPhase": workflow.get("nextRecommendedPhase", ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "")),
800
- "awaitingApproval": workflow.get("awaitingApproval", ctx.get("WORKFLOW_AWAITING_APPROVAL", "false") == "true"),
801
- "routingStatus": workflow.get("routingStatus", ctx.get("WORKFLOW_ROUTING_STATUS", "")),
980
+ "lastCompletedPhase": workflow.get(
981
+ "lastCompletedPhase", ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")
982
+ ),
983
+ "nextRecommendedPhase": workflow.get(
984
+ "nextRecommendedPhase", ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "")
985
+ ),
986
+ "awaitingApproval": workflow.get(
987
+ "awaitingApproval",
988
+ ctx.get("WORKFLOW_AWAITING_APPROVAL", "false") == "true",
989
+ ),
990
+ "routingStatus": workflow.get(
991
+ "routingStatus", ctx.get("WORKFLOW_ROUTING_STATUS", "")
992
+ ),
802
993
  "lastSafeCheckpoint": workflow.get("lastSafeCheckpoint", {}),
803
994
  },
804
995
  "teamContract": {
@@ -810,7 +1001,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
810
1001
  "finalSynthesisOwner": "Claude lead",
811
1002
  "requiredWorkerAttempts": reviewers,
812
1003
  "requiredWorkerRoles": required_worker_roles,
813
- "requiredAgentStatusEntries": ["Claude lead"] + [catalog[item]["role"] for item in reviewers],
1004
+ "requiredAgentStatusEntries": ["Claude lead"]
1005
+ + [catalog[item]["role"] for item in reviewers],
814
1006
  "requireDistinctLeadFromClaudeWorker": True,
815
1007
  "requireAllRequiredWorkerAttempts": True,
816
1008
  "requireGeminiWorkerAttempt": "gemini" in reviewers,
@@ -867,49 +1059,61 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
867
1059
  current_run_manifest_path = ctx.get("RUN_MANIFEST_FILE", "")
868
1060
  current_run_manifest_relative_path = ctx.get("RUN_MANIFEST_RELATIVE_PATH", "")
869
1061
  filtered = [
870
- item for item in runs
1062
+ item
1063
+ for item in runs
871
1064
  if item.get("runManifestPath") != current_run_manifest_relative_path
872
1065
  and item.get("runManifestPath") != current_run_manifest_path
873
1066
  ]
874
- workflow = task_manifest.get("workflow") if isinstance(task_manifest.get("workflow"), dict) else {}
1067
+ workflow = (
1068
+ task_manifest.get("workflow")
1069
+ if isinstance(task_manifest.get("workflow"), dict)
1070
+ else {}
1071
+ )
875
1072
  workflow = workflow or {}
876
- filtered.append({
877
- "runTimestamp": ctx.get("RUN_TIMESTAMP_ISO", ""),
878
- "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
879
- "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
880
- "runDateTimeSegment": ctx.get("RUN_DATETIME_SEGMENT", ""),
881
- "runSequencesByCategory": {
882
- "manifests": ctx.get("RUN_MANIFESTS_SEQ", ""),
883
- "prompts": ctx.get("RUN_PROMPTS_SEQ", ""),
884
- "reports": ctx.get("RUN_REPORTS_SEQ", ""),
885
- "status": ctx.get("RUN_STATUS_SEQ", ""),
886
- "state": ctx.get("RUN_STATE_SEQ", ""),
887
- "sessions": ctx.get("RUN_SESSIONS_SEQ", ""),
888
- "workerResults": ctx.get("WORKER_RESULTS_SEQ", ""),
889
- },
890
- "taskType": ctx.get("ANALYSIS_TYPE", ""),
891
- "workCategory": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
892
- "status": ctx.get("CURRENT_RUN_STATUS", ""),
893
- "taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
894
- "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
895
- "workerPromptDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
896
- "workerPromptPathByWorkerId": {item: worker_prompt_paths[item] for item in reviewers},
897
- "reportPath": ctx.get("LATEST_REPORT_RELATIVE_PATH") or ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
898
- "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
899
- "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
900
- "relatedTasks": json.loads(ctx.get("RELATED_TASKS_JSON", "[]")),
901
- "workflowSnapshot": {
902
- "phaseSequence": workflow.get("phaseSequence", []),
903
- "currentPhase": workflow.get("currentPhase", ""),
904
- "currentPhaseState": workflow.get("currentPhaseState", ""),
905
- "phaseStates": workflow.get("phaseStates", {}),
906
- "lastCompletedPhase": workflow.get("lastCompletedPhase", ""),
907
- "nextRecommendedPhase": workflow.get("nextRecommendedPhase", ""),
908
- "awaitingApproval": workflow.get("awaitingApproval", False),
909
- "routingStatus": workflow.get("routingStatus", ""),
910
- "lastSafeCheckpoint": workflow.get("lastSafeCheckpoint", {}),
911
- },
912
- })
1073
+ filtered.append(
1074
+ {
1075
+ "runTimestamp": ctx.get("RUN_TIMESTAMP_ISO", ""),
1076
+ "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
1077
+ "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
1078
+ "runDateTimeSegment": ctx.get("RUN_DATETIME_SEGMENT", ""),
1079
+ "runSequencesByCategory": {
1080
+ "manifests": ctx.get("RUN_MANIFESTS_SEQ", ""),
1081
+ "prompts": ctx.get("RUN_PROMPTS_SEQ", ""),
1082
+ "reports": ctx.get("RUN_REPORTS_SEQ", ""),
1083
+ "status": ctx.get("RUN_STATUS_SEQ", ""),
1084
+ "state": ctx.get("RUN_STATE_SEQ", ""),
1085
+ "sessions": ctx.get("RUN_SESSIONS_SEQ", ""),
1086
+ "workerResults": ctx.get("WORKER_RESULTS_SEQ", ""),
1087
+ },
1088
+ "taskType": ctx.get("ANALYSIS_TYPE", ""),
1089
+ "workCategory": task_manifest.get(
1090
+ "workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
1091
+ ),
1092
+ "status": ctx.get("CURRENT_RUN_STATUS", ""),
1093
+ "taskBriefPath": ctx.get("BRIEF_RELATIVE_PATH", ""),
1094
+ "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
1095
+ "workerPromptDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1096
+ "workerPromptPathByWorkerId": {
1097
+ item: worker_prompt_paths[item] for item in reviewers
1098
+ },
1099
+ "reportPath": ctx.get("LATEST_REPORT_RELATIVE_PATH")
1100
+ or ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
1101
+ "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1102
+ "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1103
+ "relatedTasks": json.loads(ctx.get("RELATED_TASKS_JSON", "[]")),
1104
+ "workflowSnapshot": {
1105
+ "phaseSequence": workflow.get("phaseSequence", []),
1106
+ "currentPhase": workflow.get("currentPhase", ""),
1107
+ "currentPhaseState": workflow.get("currentPhaseState", ""),
1108
+ "phaseStates": workflow.get("phaseStates", {}),
1109
+ "lastCompletedPhase": workflow.get("lastCompletedPhase", ""),
1110
+ "nextRecommendedPhase": workflow.get("nextRecommendedPhase", ""),
1111
+ "awaitingApproval": workflow.get("awaitingApproval", False),
1112
+ "routingStatus": workflow.get("routingStatus", ""),
1113
+ "lastSafeCheckpoint": workflow.get("lastSafeCheckpoint", {}),
1114
+ },
1115
+ }
1116
+ )
913
1117
  payload = {
914
1118
  "schemaVersion": "1.0",
915
1119
  "projectId": ctx.get("PROJECT_ID", ""),
@@ -930,20 +1134,35 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
930
1134
  task_manifest = json.loads(task_manifest_path.read_text(encoding="utf-8"))
931
1135
  except Exception:
932
1136
  task_manifest = {}
933
- workflow = task_manifest.get("workflow", {}) if isinstance(task_manifest.get("workflow"), dict) else {}
934
- phase_states = workflow.get("phaseStates", {}) if isinstance(workflow.get("phaseStates"), dict) else {}
1137
+ workflow = (
1138
+ task_manifest.get("workflow", {})
1139
+ if isinstance(task_manifest.get("workflow"), dict)
1140
+ else {}
1141
+ )
1142
+ phase_states = (
1143
+ workflow.get("phaseStates", {})
1144
+ if isinstance(workflow.get("phaseStates"), dict)
1145
+ else {}
1146
+ )
935
1147
  phase_order = workflow.get("phaseSequence", [])
936
1148
  if not isinstance(phase_order, list) or not phase_order:
937
1149
  phase_order = [
938
- "requirements-discovery", "error-analysis",
939
- "implementation-planning", "implementation", "final-verification",
1150
+ "requirements-discovery",
1151
+ "error-analysis",
1152
+ "implementation-planning",
1153
+ "implementation",
1154
+ "final-verification",
940
1155
  "release-handoff",
941
1156
  ]
942
1157
  phase_state_lines = [
943
1158
  f"- `{phase}`: `{phase_states.get(phase, 'not-started')}`"
944
1159
  for phase in phase_order
945
1160
  ]
946
- checkpoint = workflow.get("lastSafeCheckpoint", {}) if isinstance(workflow.get("lastSafeCheckpoint"), dict) else {}
1161
+ checkpoint = (
1162
+ workflow.get("lastSafeCheckpoint", {})
1163
+ if isinstance(workflow.get("lastSafeCheckpoint"), dict)
1164
+ else {}
1165
+ )
947
1166
  checkpoint_lines = [
948
1167
  f"- Label: `{checkpoint.get('label', 'unknown')}`",
949
1168
  f"- Run manifest: `{checkpoint.get('latestRunManifestPath', ctx.get('RUN_MANIFEST_RELATIVE_PATH', ''))}`",
@@ -951,9 +1170,21 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
951
1170
  f"- Report: `{checkpoint.get('latestReportPath', task_manifest.get('latestReportPath', '--')) or '--'}`",
952
1171
  f"- Resume command: `{checkpoint.get('latestResumeCommandPath', task_manifest.get('latestResumeCommandPath', '--')) or '--'}`",
953
1172
  ]
954
- rc = task_manifest.get("resultContract") if isinstance(task_manifest.get("resultContract"), dict) else {}
955
- cv = task_manifest.get("contractValidation") if isinstance(task_manifest.get("contractValidation"), dict) else {}
956
- art = task_manifest.get("artifacts") if isinstance(task_manifest.get("artifacts"), dict) else {}
1173
+ rc = (
1174
+ task_manifest.get("resultContract")
1175
+ if isinstance(task_manifest.get("resultContract"), dict)
1176
+ else {}
1177
+ )
1178
+ cv = (
1179
+ task_manifest.get("contractValidation")
1180
+ if isinstance(task_manifest.get("contractValidation"), dict)
1181
+ else {}
1182
+ )
1183
+ art = (
1184
+ task_manifest.get("artifacts")
1185
+ if isinstance(task_manifest.get("artifacts"), dict)
1186
+ else {}
1187
+ )
957
1188
  mapping = {
958
1189
  "{{TASK_KEY}}": task_manifest.get("taskKey", ctx.get("TASK_KEY", "")),
959
1190
  "{{TASK_TYPE}}": task_manifest.get("taskType", ctx.get("ANALYSIS_TYPE", "")),
@@ -961,44 +1192,96 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
961
1192
  "{{PROJECT_ID}}": ctx.get("PROJECT_ID", ""),
962
1193
  "{{TASK_GROUP}}": ctx.get("TASK_GROUP", ""),
963
1194
  "{{TASK_ID}}": ctx.get("TASK_ID", ""),
964
- "{{CURRENT_TASK_STATUS}}": task_manifest.get("currentStatus", ctx.get("CURRENT_TASK_STATUS", "")),
965
- "{{CURRENT_RUN_STATUS}}": task_manifest.get("latestRunStatus", ctx.get("CURRENT_RUN_STATUS", "")),
1195
+ "{{CURRENT_TASK_STATUS}}": task_manifest.get(
1196
+ "currentStatus", ctx.get("CURRENT_TASK_STATUS", "")
1197
+ ),
1198
+ "{{CURRENT_RUN_STATUS}}": task_manifest.get(
1199
+ "latestRunStatus", ctx.get("CURRENT_RUN_STATUS", "")
1200
+ ),
966
1201
  "{{RELATED_TASKS_INLINE}}": ctx.get("RELATED_TASKS_INLINE", "None"),
967
- "{{RECOMMENDED_ANALYSERS}}": ", ".join(task_manifest.get("recommendedWorkers", [])),
1202
+ "{{RECOMMENDED_ANALYSERS}}": ", ".join(
1203
+ task_manifest.get("recommendedWorkers", [])
1204
+ ),
968
1205
  "{{LEAD_MODEL}}": rc.get("leadModel", ctx.get("LEAD_MODEL_DISPLAY", "")),
969
1206
  "{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
970
- "{{LATEST_RUN_RELATIVE_PATH}}": task_manifest.get("latestRunPath", ctx.get("LATEST_RUN_RELATIVE_PATH", "")),
971
- "{{LATEST_REPORT_RELATIVE_PATH}}": task_manifest.get("latestReportPath", ctx.get("LATEST_REPORT_RELATIVE_PATH", "")),
972
- "{{TEAM_STATE_RELATIVE_PATH}}": task_manifest.get("teamStatePath", ctx.get("TEAM_STATE_RELATIVE_PATH", "")),
973
- "{{VALIDATION_STATUS}}": cv.get("status", ctx.get("VALIDATION_STATUS", "not-run")),
974
- "{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": task_manifest.get("latestResumeCommandPath", ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "")),
975
- "{{MODEL_ASSIGNMENT_LINES}}": "\n".join([
976
- f"- `Claude lead`: `{rc.get('leadModel', ctx.get('LEAD_MODEL_DISPLAY', ''))}`",
977
- f"- `Claude worker`: `{ctx.get('CLAUDE_WORKER_MODEL_DISPLAY', '')}`",
978
- f"- `Codex worker`: `{ctx.get('CODEX_WORKER_MODEL_DISPLAY', '')}`",
979
- f"- `Gemini worker`: `{ctx.get('GEMINI_WORKER_MODEL_DISPLAY', '')}`",
980
- f"- `Report writer worker`: `{ctx.get('REPORT_WRITER_MODEL_DISPLAY', '')}`",
981
- ]),
982
- "{{TASK_MANIFEST_RELATIVE_PATH}}": task_manifest.get("taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")),
983
- "{{OKSTRA_LATEST_TASK_RELATIVE_PATH}}": ctx.get("OKSTRA_LATEST_TASK_RELATIVE_PATH", ""),
984
- "{{INSTRUCTION_SET_RELATIVE_PATH}}": task_manifest.get("instructionSetPath", ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")),
985
- "{{REFERENCE_EXPECTATIONS_RELATIVE_PATH}}": task_manifest.get("referenceExpectationsPath", ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", "")),
986
- "{{FINAL_REPORT_TEMPLATE_RELATIVE_PATH}}": art.get("finalReportTemplatePath", ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", "")),
1207
+ "{{LATEST_RUN_RELATIVE_PATH}}": task_manifest.get(
1208
+ "latestRunPath", ctx.get("LATEST_RUN_RELATIVE_PATH", "")
1209
+ ),
1210
+ "{{LATEST_REPORT_RELATIVE_PATH}}": task_manifest.get(
1211
+ "latestReportPath", ctx.get("LATEST_REPORT_RELATIVE_PATH", "")
1212
+ ),
1213
+ "{{TEAM_STATE_RELATIVE_PATH}}": task_manifest.get(
1214
+ "teamStatePath", ctx.get("TEAM_STATE_RELATIVE_PATH", "")
1215
+ ),
1216
+ "{{VALIDATION_STATUS}}": cv.get(
1217
+ "status", ctx.get("VALIDATION_STATUS", "not-run")
1218
+ ),
1219
+ "{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": task_manifest.get(
1220
+ "latestResumeCommandPath",
1221
+ ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1222
+ ),
1223
+ "{{MODEL_ASSIGNMENT_LINES}}": "\n".join(
1224
+ [
1225
+ f"- `Claude lead`: `{rc.get('leadModel', ctx.get('LEAD_MODEL_DISPLAY', ''))}`",
1226
+ f"- `Claude worker`: `{ctx.get('CLAUDE_WORKER_MODEL_DISPLAY', '')}`",
1227
+ f"- `Codex worker`: `{ctx.get('CODEX_WORKER_MODEL_DISPLAY', '')}`",
1228
+ f"- `Gemini worker`: `{ctx.get('GEMINI_WORKER_MODEL_DISPLAY', '')}`",
1229
+ f"- `Report writer worker`: `{ctx.get('REPORT_WRITER_MODEL_DISPLAY', '')}`",
1230
+ ]
1231
+ ),
1232
+ "{{TASK_MANIFEST_RELATIVE_PATH}}": task_manifest.get(
1233
+ "taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")
1234
+ ),
1235
+ "{{OKSTRA_LATEST_TASK_RELATIVE_PATH}}": ctx.get(
1236
+ "OKSTRA_LATEST_TASK_RELATIVE_PATH", ""
1237
+ ),
1238
+ "{{INSTRUCTION_SET_RELATIVE_PATH}}": task_manifest.get(
1239
+ "instructionSetPath", ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1240
+ ),
1241
+ "{{REFERENCE_EXPECTATIONS_RELATIVE_PATH}}": task_manifest.get(
1242
+ "referenceExpectationsPath",
1243
+ ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
1244
+ ),
1245
+ "{{FINAL_REPORT_TEMPLATE_RELATIVE_PATH}}": art.get(
1246
+ "finalReportTemplatePath",
1247
+ ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
1248
+ ),
987
1249
  "{{RUN_MANIFESTS_RELATIVE_PATH}}": ctx.get("RUN_MANIFESTS_RELATIVE_PATH", ""),
988
1250
  "{{RUN_STATE_RELATIVE_PATH}}": ctx.get("RUN_STATE_RELATIVE_PATH", ""),
989
- "{{RUN_PROMPTS_RELATIVE_PATH}}": task_manifest.get("latestRunPromptsPath", ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")),
1251
+ "{{RUN_PROMPTS_RELATIVE_PATH}}": task_manifest.get(
1252
+ "latestRunPromptsPath", ctx.get("RUN_PROMPTS_RELATIVE_PATH", "")
1253
+ ),
990
1254
  "{{RUN_REPORTS_RELATIVE_PATH}}": ctx.get("RUN_REPORTS_RELATIVE_PATH", ""),
991
1255
  "{{RUN_STATUS_RELATIVE_PATH}}": ctx.get("RUN_STATUS_RELATIVE_PATH", ""),
992
1256
  "{{RUN_SESSIONS_RELATIVE_PATH}}": ctx.get("RUN_SESSIONS_RELATIVE_PATH", ""),
993
- "{{WORKER_RESULTS_RELATIVE_PATH}}": art.get("workerResultsDirectoryPath", ctx.get("WORKER_RESULTS_RELATIVE_PATH", "")),
994
- "{{RUN_VALIDATOR_RELATIVE_PATH}}": cv.get("validatorScriptPath", ctx.get("RUN_VALIDATOR_RELATIVE_PATH", "")),
995
- "{{WORK_CATEGORY}}": task_manifest.get("workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")),
996
- "{{WORKFLOW_CURRENT_PHASE}}": workflow.get("currentPhase", ctx.get("WORKFLOW_CURRENT_PHASE", "")),
997
- "{{WORKFLOW_CURRENT_PHASE_STATE}}": workflow.get("currentPhaseState", ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "")),
998
- "{{WORKFLOW_LAST_COMPLETED_PHASE}}": workflow.get("lastCompletedPhase", ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")) or "--",
999
- "{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}": workflow.get("nextRecommendedPhase", ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "")),
1000
- "{{WORKFLOW_AWAITING_APPROVAL}}": "yes" if workflow.get("awaitingApproval", False) else "no",
1001
- "{{WORKFLOW_ROUTING_STATUS}}": workflow.get("routingStatus", ctx.get("WORKFLOW_ROUTING_STATUS", "")),
1257
+ "{{WORKER_RESULTS_RELATIVE_PATH}}": art.get(
1258
+ "workerResultsDirectoryPath", ctx.get("WORKER_RESULTS_RELATIVE_PATH", "")
1259
+ ),
1260
+ "{{RUN_VALIDATOR_RELATIVE_PATH}}": cv.get(
1261
+ "validatorScriptPath", ctx.get("RUN_VALIDATOR_RELATIVE_PATH", "")
1262
+ ),
1263
+ "{{WORK_CATEGORY}}": task_manifest.get(
1264
+ "workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
1265
+ ),
1266
+ "{{WORKFLOW_CURRENT_PHASE}}": workflow.get(
1267
+ "currentPhase", ctx.get("WORKFLOW_CURRENT_PHASE", "")
1268
+ ),
1269
+ "{{WORKFLOW_CURRENT_PHASE_STATE}}": workflow.get(
1270
+ "currentPhaseState", ctx.get("WORKFLOW_CURRENT_PHASE_STATE", "")
1271
+ ),
1272
+ "{{WORKFLOW_LAST_COMPLETED_PHASE}}": workflow.get(
1273
+ "lastCompletedPhase", ctx.get("WORKFLOW_LAST_COMPLETED_PHASE", "")
1274
+ )
1275
+ or "--",
1276
+ "{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}": workflow.get(
1277
+ "nextRecommendedPhase", ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", "")
1278
+ ),
1279
+ "{{WORKFLOW_AWAITING_APPROVAL}}": "yes"
1280
+ if workflow.get("awaitingApproval", False)
1281
+ else "no",
1282
+ "{{WORKFLOW_ROUTING_STATUS}}": workflow.get(
1283
+ "routingStatus", ctx.get("WORKFLOW_ROUTING_STATUS", "")
1284
+ ),
1002
1285
  "{{WORKFLOW_PHASE_STATE_LINES}}": "\n".join(phase_state_lines),
1003
1286
  "{{WORKFLOW_LAST_SAFE_CHECKPOINT_LINES}}": "\n".join(checkpoint_lines),
1004
1287
  }
@@ -1008,6 +1291,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
1008
1291
  rendered = template
1009
1292
  for k, v in mapping.items():
1010
1293
  rendered = rendered.replace(k, v)
1294
+ rendered = _strip_phase_blocks(rendered, ctx.get("ANALYSIS_TYPE", ""))
1011
1295
  _write_text(Path(output_path), rendered.rstrip() + "\n")
1012
1296
 
1013
1297
 
@@ -1055,7 +1339,9 @@ def build_available_mcp_servers_block(project_root: Path) -> str:
1055
1339
  if description:
1056
1340
  parts.append(description)
1057
1341
  if isinstance(tools, list) and tools:
1058
- tool_names = ", ".join(f"`{str(t).strip()}`" for t in tools if str(t).strip())
1342
+ tool_names = ", ".join(
1343
+ f"`{str(t).strip()}`" for t in tools if str(t).strip()
1344
+ )
1059
1345
  if tool_names:
1060
1346
  parts.append(f"Tools: {tool_names}")
1061
1347
  if notes:
@@ -1083,7 +1369,9 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1083
1369
 
1084
1370
  worker_result_lines = []
1085
1371
  team_role_lines = [f" 1. `Claude lead` (assigned model: `{lead_model}`)"]
1086
- model_assignment_lines = [fmt_assignment("Claude lead", lead_model, lead_model_execution)]
1372
+ model_assignment_lines = [
1373
+ fmt_assignment("Claude lead", lead_model, lead_model_execution)
1374
+ ]
1087
1375
  worker_role_labels = []
1088
1376
  execution_status_entries = ["`Claude lead`"]
1089
1377
  execution_status_table_lines = [
@@ -1094,24 +1382,34 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1094
1382
  for index, worker in enumerate(selected, start=2):
1095
1383
  m = catalog[worker]
1096
1384
  worker_result_lines.append(
1097
- f"- {m['role']} result path: `{m['resultPath']}` (assigned model: `{m['model']}`)")
1098
- team_role_lines.append(f" {index}. `{m['role']}` (assigned model: `{m['model']}`)")
1099
- model_assignment_lines.append(fmt_assignment(m["role"], m["model"], m["modelExecutionValue"]))
1385
+ f"- {m['role']} result path: `{m['resultPath']}` (assigned model: `{m['model']}`)"
1386
+ )
1387
+ team_role_lines.append(
1388
+ f" {index}. `{m['role']}` (assigned model: `{m['model']}`)"
1389
+ )
1390
+ model_assignment_lines.append(
1391
+ fmt_assignment(m["role"], m["model"], m["modelExecutionValue"])
1392
+ )
1100
1393
  worker_role_labels.append(f"`{m['role']}`")
1101
1394
  execution_status_entries.append(f"`{m['role']}`")
1102
1395
  execution_status_table_lines.append(
1103
- f"| {m['agentLabel']} | {m['role']} | {m['model']} | completed / timeout / error / not-run | {m['role']}의 핵심 발견 요약 |")
1396
+ f"| {m['agentLabel']} | {m['role']} | {m['model']} | completed / timeout / error / not-run | {m['role']}의 핵심 발견 요약 |"
1397
+ )
1104
1398
 
1105
1399
  if worker_role_labels:
1106
1400
  if len(worker_role_labels) == 1:
1107
- worker_role_sentence = f"- {worker_role_labels[0]} is the required worker role."
1401
+ worker_role_sentence = (
1402
+ f"- {worker_role_labels[0]} is the required worker role."
1403
+ )
1108
1404
  else:
1109
1405
  worker_role_sentence = (
1110
1406
  f"- {', '.join(worker_role_labels[:-1])}, "
1111
- f"and {worker_role_labels[-1]} are the required worker roles.")
1407
+ f"and {worker_role_labels[-1]} are the required worker roles."
1408
+ )
1112
1409
  preferred_results_sentence = (
1113
1410
  f"- Aim to collect completed results from all "
1114
- f"{len(worker_role_labels)} required workers.")
1411
+ f"{len(worker_role_labels)} required workers."
1412
+ )
1115
1413
  else:
1116
1414
  worker_role_sentence = "- No worker roles were selected for this run."
1117
1415
  preferred_results_sentence = "- No worker results are expected for this run."
@@ -1144,7 +1442,7 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1144
1442
  "## Team Creation Gate (BLOCKING)\n"
1145
1443
  "\n"
1146
1444
  "Before any `Agent` dispatch for workers, you MUST perform Phase 3 of the\n"
1147
- "`okstra` skill (`agents/SKILL.md` → \"Phase 3 — Team creation\"). Skipping\n"
1445
+ '`okstra` skill (`agents/SKILL.md` → "Phase 3 — Team creation"). Skipping\n'
1148
1446
  "this gate silently degrades the run to in-process background dispatch and\n"
1149
1447
  "loses the Teams split-pane observability surface, even though worker\n"
1150
1448
  "outputs may still appear correct on disk.\n"
@@ -1155,16 +1453,16 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1155
1453
  "\n"
1156
1454
  "1. Invoke the `okstra-team-contract` skill and verify the selected worker\n"
1157
1455
  " roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.\n"
1158
- f"2. Call `TeamCreate(team_name: \"okstra-{ctx.get('TASK_KEY', '')}\", description: ...)`.\n"
1456
+ f'2. Call `TeamCreate(team_name: "okstra-{ctx.get("TASK_KEY", "")}", description: ...)`.\n'
1159
1457
  "3. Record the outcome in team-state under\n"
1160
- " `teamCreate: { attempted: true, status: \"ok\" | \"error\", error?: <msg> }`\n"
1458
+ ' `teamCreate: { attempted: true, status: "ok" | "error", error?: <msg> }`\n'
1161
1459
  " BEFORE any `Agent(...)` worker dispatch.\n"
1162
1460
  "4. Only after `teamCreate` is persisted may you dispatch workers — with\n"
1163
1461
  " `team_name` on success, or with `run_in_background: true` and no\n"
1164
- " `team_name` ONLY when `teamCreate.status == \"error\"` was recorded.\n"
1462
+ ' `team_name` ONLY when `teamCreate.status == "error"` was recorded.\n'
1165
1463
  "\n"
1166
- "If the Agent tool rejects a dispatch with `\"team must be created first\"` /\n"
1167
- "`\"team을 먼저 생성하거나 team_name 없이 호출해야 합니다\"`, the correct\n"
1464
+ 'If the Agent tool rejects a dispatch with `"team must be created first"` /\n'
1465
+ '`"team을 먼저 생성하거나 team_name 없이 호출해야 합니다"`, the correct\n'
1168
1466
  "response is to go back to step 2 — NOT to strip `team_name` and retry."
1169
1467
  )
1170
1468
 
@@ -1178,7 +1476,9 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1178
1476
  "{{BRIEF_RELATIVE_PATH}}": ctx.get("BRIEF_RELATIVE_PATH", ""),
1179
1477
  "{{BRIEF_FILE_PATH}}": ctx.get("BRIEF_FILE_PATH", ""),
1180
1478
  "{{CLARIFICATION_RESPONSE_PATH}}": ctx.get("CLARIFICATION_RESPONSE_FILE", ""),
1181
- "{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}": ctx.get("CLARIFICATION_RESPONSE_RELATIVE_PATH", ""),
1479
+ "{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}": ctx.get(
1480
+ "CLARIFICATION_RESPONSE_RELATIVE_PATH", ""
1481
+ ),
1182
1482
  "{{RUN_DIR}}": ctx.get("RUN_DIR", ""),
1183
1483
  "{{RUN_DIR_RELATIVE_PATH}}": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
1184
1484
  "{{RUN_MANIFESTS_RELATIVE_PATH}}": ctx.get("RUN_MANIFESTS_RELATIVE_PATH", ""),
@@ -1205,31 +1505,63 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1205
1505
  "{{WORKER_RESULTS_RELATIVE_PATH}}": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
1206
1506
  "{{RUN_VALIDATOR_PATH}}": ctx.get("RUN_VALIDATOR_SCRIPT", ""),
1207
1507
  "{{RUN_VALIDATOR_RELATIVE_PATH}}": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
1208
- "{{CLAUDE_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("CLAUDE_WORKER_RESULT_RELATIVE_PATH", ""),
1209
- "{{CODEX_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("CODEX_WORKER_RESULT_RELATIVE_PATH", ""),
1210
- "{{GEMINI_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("GEMINI_WORKER_RESULT_RELATIVE_PATH", ""),
1211
- "{{REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH}}": ctx.get("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", ""),
1508
+ "{{CLAUDE_WORKER_RESULT_RELATIVE_PATH}}": ctx.get(
1509
+ "CLAUDE_WORKER_RESULT_RELATIVE_PATH", ""
1510
+ ),
1511
+ "{{CODEX_WORKER_RESULT_RELATIVE_PATH}}": ctx.get(
1512
+ "CODEX_WORKER_RESULT_RELATIVE_PATH", ""
1513
+ ),
1514
+ "{{GEMINI_WORKER_RESULT_RELATIVE_PATH}}": ctx.get(
1515
+ "GEMINI_WORKER_RESULT_RELATIVE_PATH", ""
1516
+ ),
1517
+ "{{REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH}}": ctx.get(
1518
+ "REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", ""
1519
+ ),
1212
1520
  "{{RUN_ERRORS_LOG_PATH}}": ctx.get("RUN_ERRORS_LOG_FILE", ""),
1213
1521
  "{{RUN_ERRORS_LOG_RELATIVE_PATH}}": ctx.get("RUN_ERRORS_LOG_RELATIVE_PATH", ""),
1214
- "{{CLAUDE_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get("CLAUDE_WORKER_ERRORS_SIDECAR_FILE", ""),
1215
- "{{CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get("CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
1216
- "{{CODEX_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get("CODEX_WORKER_ERRORS_SIDECAR_FILE", ""),
1217
- "{{CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get("CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
1218
- "{{GEMINI_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get("GEMINI_WORKER_ERRORS_SIDECAR_FILE", ""),
1219
- "{{GEMINI_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get("GEMINI_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
1220
- "{{REPORT_WRITER_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get("REPORT_WRITER_WORKER_ERRORS_SIDECAR_FILE", ""),
1221
- "{{REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get("REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
1522
+ "{{CLAUDE_WORKER_ERRORS_SIDECAR_PATH}}": ctx.get(
1523
+ "CLAUDE_WORKER_ERRORS_SIDECAR_FILE", ""
1524
+ ),
1525
+ "{{CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH}}": ctx.get(
1526
+ "CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""
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
+ ),
1222
1546
  "{{LEAD_MODEL}}": lead_model,
1223
1547
  "{{LEAD_MODEL_EXECUTION_VALUE}}": lead_model_execution,
1224
1548
  "{{OKSTRA_VERSION}}": ctx.get("OKSTRA_VERSION", ""),
1225
1549
  "{{CLAUDE_WORKER_MODEL}}": ctx.get("CLAUDE_WORKER_MODEL_DISPLAY", ""),
1226
- "{{CLAUDE_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""),
1550
+ "{{CLAUDE_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get(
1551
+ "CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""
1552
+ ),
1227
1553
  "{{CODEX_WORKER_MODEL}}": ctx.get("CODEX_WORKER_MODEL_DISPLAY", ""),
1228
- "{{CODEX_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("CODEX_WORKER_MODEL_EXECUTION_VALUE", ""),
1554
+ "{{CODEX_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get(
1555
+ "CODEX_WORKER_MODEL_EXECUTION_VALUE", ""
1556
+ ),
1229
1557
  "{{GEMINI_WORKER_MODEL}}": ctx.get("GEMINI_WORKER_MODEL_DISPLAY", ""),
1230
- "{{GEMINI_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get("GEMINI_WORKER_MODEL_EXECUTION_VALUE", ""),
1558
+ "{{GEMINI_WORKER_MODEL_EXECUTION_VALUE}}": ctx.get(
1559
+ "GEMINI_WORKER_MODEL_EXECUTION_VALUE", ""
1560
+ ),
1231
1561
  "{{REPORT_WRITER_MODEL}}": ctx.get("REPORT_WRITER_MODEL_DISPLAY", ""),
1232
- "{{REPORT_WRITER_MODEL_EXECUTION_VALUE}}": ctx.get("REPORT_WRITER_MODEL_EXECUTION_VALUE", ""),
1562
+ "{{REPORT_WRITER_MODEL_EXECUTION_VALUE}}": ctx.get(
1563
+ "REPORT_WRITER_MODEL_EXECUTION_VALUE", ""
1564
+ ),
1233
1565
  "{{WORKER_RESULT_PATH_LINES}}": "\n".join(worker_result_lines),
1234
1566
  "{{MODEL_ASSIGNMENT_LINES}}": "\n".join(model_assignment_lines),
1235
1567
  "{{TEAM_ROLE_LINES}}": "\n".join(team_role_lines),
@@ -1239,30 +1571,48 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1239
1571
  "{{EXECUTION_STATUS_EXACT_ENTRIES}}": ", ".join(execution_status_entries),
1240
1572
  "{{EXECUTION_STATUS_TABLE_ROWS}}": "\n".join(execution_status_table_lines),
1241
1573
  "{{FINAL_REPORT_TEMPLATE_PATH}}": ctx.get("FINAL_REPORT_TEMPLATE_FILE", ""),
1242
- "{{FINAL_REPORT_TEMPLATE_RELATIVE_PATH}}": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
1243
- "{{REFERENCE_EXPECTATIONS_RELATIVE_PATH}}": ctx.get("REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""),
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
+ ),
1244
1580
  "{{CLAUDE_SESSION_ID}}": ctx.get("CLAUDE_SESSION_ID", ""),
1245
1581
  "{{CLAUDE_RESUME_COMMAND_PATH}}": ctx.get("CLAUDE_RESUME_COMMAND_FILE", ""),
1246
- "{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1582
+ "{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": ctx.get(
1583
+ "CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""
1584
+ ),
1247
1585
  "{{TASK_ROOT_RELATIVE_PATH}}": ctx.get("TASK_ROOT_RELATIVE_PATH", ""),
1248
1586
  "{{TASK_MANIFEST_RELATIVE_PATH}}": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
1249
1587
  "{{TASK_INDEX_RELATIVE_PATH}}": ctx.get("TASK_INDEX_RELATIVE_PATH", ""),
1250
- "{{INSTRUCTION_SET_RELATIVE_PATH}}": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", ""),
1588
+ "{{INSTRUCTION_SET_RELATIVE_PATH}}": ctx.get(
1589
+ "INSTRUCTION_SET_RELATIVE_PATH", ""
1590
+ ),
1251
1591
  "{{RUNS_RELATIVE_PATH}}": ctx.get("RUNS_RELATIVE_PATH", ""),
1252
1592
  "{{HISTORY_RELATIVE_PATH}}": ctx.get("HISTORY_RELATIVE_PATH", ""),
1253
- "{{OKSTRA_DISCOVERY_RELATIVE_PATH}}": ctx.get("OKSTRA_DISCOVERY_RELATIVE_PATH", ""),
1254
- "{{OKSTRA_LATEST_TASK_RELATIVE_PATH}}": ctx.get("OKSTRA_LATEST_TASK_RELATIVE_PATH", ""),
1255
- "{{OKSTRA_TASK_CATALOG_RELATIVE_PATH}}": ctx.get("OKSTRA_TASK_CATALOG_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
+ ),
1256
1602
  "{{LATEST_RUN_RELATIVE_PATH}}": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
1257
1603
  "{{LATEST_REPORT_RELATIVE_PATH}}": ctx.get("LATEST_REPORT_RELATIVE_PATH", ""),
1258
1604
  "{{TIMELINE_RELATIVE_PATH}}": ctx.get("TIMELINE_RELATIVE_PATH", ""),
1259
1605
  "{{CURRENT_TASK_STATUS}}": ctx.get("CURRENT_TASK_STATUS", ""),
1260
1606
  "{{CURRENT_RUN_STATUS}}": ctx.get("CURRENT_RUN_STATUS", ""),
1261
1607
  "{{VALIDATION_STATUS}}": ctx.get("VALIDATION_STATUS", "not-run"),
1262
- "{{RELATED_TASKS_BULLETS}}": ctx.get("RELATED_TASKS_BULLETS", "- None recorded"),
1608
+ "{{RELATED_TASKS_BULLETS}}": ctx.get(
1609
+ "RELATED_TASKS_BULLETS", "- None recorded"
1610
+ ),
1263
1611
  "{{RELATED_TASKS_INLINE}}": ctx.get("RELATED_TASKS_INLINE", "None"),
1264
1612
  "{{WORKFLOW_CURRENT_PHASE}}": ctx.get("WORKFLOW_CURRENT_PHASE", ""),
1265
- "{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}": ctx.get("WORKFLOW_NEXT_RECOMMENDED_PHASE", ""),
1613
+ "{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}": ctx.get(
1614
+ "WORKFLOW_NEXT_RECOMMENDED_PHASE", ""
1615
+ ),
1266
1616
  "{{PHASE_ALLOWED_OUTPUTS}}": ctx.get("PHASE_ALLOWED_OUTPUTS", ""),
1267
1617
  "{{PHASE_FORBIDDEN_ACTIONS}}": ctx.get("PHASE_FORBIDDEN_ACTIONS", ""),
1268
1618
  "{{AVAILABLE_MCP_SERVERS}}": ctx.get(
@@ -1281,6 +1631,7 @@ def render_template_file(template_path: str, output_path: str, ctx: dict) -> Non
1281
1631
  rendered = template
1282
1632
  for k, v in mapping.items():
1283
1633
  rendered = rendered.replace(k, v)
1634
+ rendered = _strip_phase_blocks(rendered, ctx.get("ANALYSIS_TYPE", ""))
1284
1635
  _write_text(Path(output_path), rendered.rstrip() + "\n")
1285
1636
 
1286
1637