okstra 0.167.0 → 0.169.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +6 -5
  2. package/docs/architecture/storage-model.md +57 -1
  3. package/docs/architecture.md +70 -2
  4. package/docs/cli.md +8 -4
  5. package/docs/for-ai/skills/okstra-code-review.md +3 -2
  6. package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
  7. package/docs/pr-template-usage.md +10 -6
  8. package/docs/project-structure-overview.md +14 -11
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/claude-worker.md +6 -5
  12. package/runtime/agents/workers/report-writer-worker.md +9 -4
  13. package/runtime/agents/workers/translator-worker.md +6 -4
  14. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
  15. package/runtime/prompts/coding-preflight/clean-code.md +13 -0
  16. package/runtime/prompts/duties/acceptance-critic.md +24 -0
  17. package/runtime/prompts/duties/acceptance-verifier.md +24 -0
  18. package/runtime/prompts/duties/analysis-worker.md +24 -0
  19. package/runtime/prompts/duties/code-reviewer.md +24 -0
  20. package/runtime/prompts/duties/common.md +35 -0
  21. package/runtime/prompts/duties/implementation-executor.md +24 -0
  22. package/runtime/prompts/duties/implementation-verifier.md +24 -0
  23. package/runtime/prompts/duties/lead.md +24 -0
  24. package/runtime/prompts/duties/report-writer.md +24 -0
  25. package/runtime/prompts/duties/reverification-worker.md +24 -0
  26. package/runtime/prompts/duties/schedule-verifier.md +24 -0
  27. package/runtime/prompts/duties/scope-critic.md +24 -0
  28. package/runtime/prompts/duties/translator.md +24 -0
  29. package/runtime/prompts/lead/convergence.md +51 -7
  30. package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
  31. package/runtime/prompts/lead/plan-body-verification.md +16 -1
  32. package/runtime/prompts/lead/report-writer.md +20 -5
  33. package/runtime/prompts/lead/team-contract.md +13 -13
  34. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  35. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  36. package/runtime/prompts/profiles/_implementation-executor.md +1 -1
  37. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  38. package/runtime/prompts/profiles/implementation.md +4 -2
  39. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
  40. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
  41. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
  42. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
  43. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
  44. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
  45. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
  46. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
  47. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
  48. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
  49. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
  50. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
  51. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
  52. package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
  53. package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
  54. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
  55. package/runtime/python/okstra_ctl/context_cost.py +46 -5
  56. package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
  57. package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
  58. package/runtime/python/okstra_ctl/doctor.py +150 -16
  59. package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
  60. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
  61. package/runtime/python/okstra_ctl/path_hints.py +26 -0
  62. package/runtime/python/okstra_ctl/paths.py +20 -0
  63. package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
  64. package/runtime/python/okstra_ctl/ports/host.py +3 -0
  65. package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
  66. package/runtime/python/okstra_ctl/pr_template.py +3 -6
  67. package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
  68. package/runtime/python/okstra_ctl/render.py +217 -12
  69. package/runtime/python/okstra_ctl/report_finalize.py +44 -0
  70. package/runtime/python/okstra_ctl/run.py +368 -51
  71. package/runtime/python/okstra_ctl/session.py +16 -12
  72. package/runtime/python/okstra_ctl/team.py +11 -11
  73. package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
  74. package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
  75. package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
  76. package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
  77. package/runtime/skills/okstra-code-review/SKILL.md +22 -3
  78. package/runtime/skills/okstra-run/SKILL.md +16 -1
  79. package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
  80. package/runtime/templates/implementation-worker-preamble.md +0 -10
  81. package/runtime/templates/report-writer-prompt-preamble.md +0 -9
  82. package/runtime/templates/reports/settings.template.json +0 -11
  83. package/runtime/templates/worker-prompt-preamble.md +0 -10
  84. package/runtime/validators/lib/fixtures.sh +93 -0
  85. package/runtime/validators/lib/validate-assets.sh +0 -8
  86. package/runtime/validators/validate-run.py +182 -0
  87. package/src/cli-registry.mjs +14 -0
  88. package/src/commands/execute/agent-prompt.mjs +25 -0
  89. package/src/commands/execute/codex-dispatch.mjs +6 -63
  90. package/src/commands/execute/worker-dispatch.mjs +76 -0
  91. package/src/commands/lifecycle/doctor.mjs +18 -3
  92. package/src/commands/lifecycle/install.mjs +33 -15
  93. package/src/commands/lifecycle/uninstall.mjs +4 -3
  94. package/src/lib/install-assets.mjs +9 -0
  95. package/runtime/agents/workers/antigravity-worker.md +0 -259
  96. package/runtime/agents/workers/codex-worker.md +0 -259
  97. package/runtime/agents/workers/grok-worker.md +0 -259
  98. package/runtime/agents/workers/kimi-worker.md +0 -259
  99. package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
  100. package/runtime/templates/operating-standard.md +0 -22
  101. package/src/lib/worker-agent-render.mjs +0 -50
  102. /package/runtime/templates/{prd → pr}/pr-body.template.md +0 -0
@@ -21,6 +21,7 @@ import hashlib
21
21
  import json
22
22
  import re
23
23
  import sys
24
+ from collections.abc import Mapping
24
25
  from pathlib import Path
25
26
 
26
27
  from okstra_project.dirs import TASK_MANIFEST_FILENAME, OKSTRA_DIR_NAME, project_json_path
@@ -354,6 +355,58 @@ def _lead_assignment(ctx: dict) -> dict:
354
355
  }
355
356
 
356
357
 
358
+ def _invocation_assignments(ctx: dict) -> dict:
359
+ raw = ctx.get("INVOCATION_ASSIGNMENTS_JSON", "")
360
+ try:
361
+ parsed = json.loads(raw) if raw else {}
362
+ except json.JSONDecodeError as exc:
363
+ raise ValueError("invalid invocation assignments JSON") from exc
364
+ if not isinstance(parsed, dict):
365
+ raise ValueError("invocation assignments must be an object")
366
+ return parsed
367
+
368
+
369
+ def _agent_contract(ctx: dict) -> dict:
370
+ raw = ctx.get("AGENT_CONTRACT_JSON", "")
371
+ try:
372
+ parsed = json.loads(raw) if raw else {}
373
+ except json.JSONDecodeError as exc:
374
+ raise ValueError("invalid agent contract JSON") from exc
375
+ if not isinstance(parsed, dict):
376
+ raise ValueError("agent contract must be an object")
377
+ return parsed
378
+
379
+
380
+ def _executor_contract(ctx: dict) -> dict:
381
+ runner = ctx.get("EXECUTOR_RUNNER", "")
382
+ dispatch_mode = ctx.get("EXECUTOR_DISPATCH_MODE", "")
383
+ host_model_value = ctx.get("EXECUTOR_HOST_MODEL_VALUE") or None
384
+ expected_mode = {
385
+ "native-session": "host-native",
386
+ "cli-wrapper": "worker-dispatch",
387
+ }.get(runner)
388
+ if expected_mode is None or dispatch_mode != expected_mode:
389
+ raise ValueError(
390
+ "executor runner and dispatch mode must be native-session/host-native "
391
+ "or cli-wrapper/worker-dispatch"
392
+ )
393
+ if runner == "native-session" and not host_model_value:
394
+ raise ValueError("native-session executor requires host model value")
395
+ if runner == "cli-wrapper" and host_model_value is not None:
396
+ raise ValueError("cli-wrapper executor must not define host model value")
397
+ return {
398
+ "workerId": ctx.get("EXECUTOR_WORKER_ID", ""),
399
+ "provider": ctx.get("EXECUTOR_PROVIDER", ""),
400
+ "displayName": ctx.get("EXECUTOR_DISPLAY_NAME", ""),
401
+ "model": ctx.get("EXECUTOR_MODEL_DISPLAY", ""),
402
+ "modelExecutionValue": ctx.get("EXECUTOR_MODEL_EXECUTION_VALUE", ""),
403
+ "hostModelValue": host_model_value,
404
+ "runner": runner,
405
+ "dispatchMode": dispatch_mode,
406
+ "appliesTo": "implementation",
407
+ }
408
+
409
+
357
410
  def _worker_assignments(ctx: dict) -> list[dict]:
358
411
  raw = ctx.get("WORKER_ASSIGNMENTS_JSON", "")
359
412
  if raw:
@@ -648,6 +701,7 @@ def render_active_run_context(active_context_path: str, ctx: dict) -> None:
648
701
  "task": _active_task(ctx),
649
702
  "workflow": _active_workflow(ctx),
650
703
  "run": _active_run(ctx),
704
+ "agentContract": _agent_contract(ctx),
651
705
  "instructionSet": _active_instruction_set(ctx),
652
706
  "workers": _active_workers(ctx),
653
707
  "errorLogs": _active_error_logs(ctx),
@@ -690,6 +744,8 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
690
744
  "leadRuntime": _lead_runtime(ctx),
691
745
  "leadAssignment": _lead_assignment(ctx),
692
746
  "workerAssignments": _worker_assignments(ctx),
747
+ "agentContract": _agent_contract(ctx),
748
+ "invocationAssignments": _invocation_assignments(ctx),
693
749
  "leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
694
750
  "leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
695
751
  "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
@@ -1341,10 +1397,8 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1341
1397
  "referenceExpectationsPath": ctx.get(
1342
1398
  "REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""
1343
1399
  ),
1344
- "leadExecutionPromptPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1345
- + "/lead-execution-prompt.md",
1346
- "claudeExecutionPromptPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1347
- + "/claude-execution-prompt.md",
1400
+ "leadExecutionPromptPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
1401
+ "claudeExecutionPromptPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
1348
1402
  "leadPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
1349
1403
  "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1350
1404
  "workerPromptPathByWorkerId": worker_prompt_paths,
@@ -1567,6 +1621,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1567
1621
  "runtimeResolution": _runtime_resolution(ctx),
1568
1622
  "leadAssignment": _lead_assignment(ctx),
1569
1623
  "workerAssignments": _worker_assignments(ctx),
1624
+ "agentContract": _agent_contract(ctx),
1625
+ "invocationAssignments": _invocation_assignments(ctx),
1570
1626
  "leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
1571
1627
  "workCategory": task_manifest.get(
1572
1628
  "workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
@@ -1595,6 +1651,33 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1595
1651
  "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
1596
1652
  "leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
1597
1653
  "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
1654
+ "leadInstructionsPath": ctx.get("LEAD_INSTRUCTIONS_RELATIVE_PATH", ""),
1655
+ "leadExecutionPromptPath": ctx.get(
1656
+ "RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""
1657
+ ),
1658
+ "leadPromptMetadataPath": ctx.get(
1659
+ "LEAD_PROMPT_METADATA_RELATIVE_PATH", ""
1660
+ ),
1661
+ "resources": {
1662
+ "leadInstructionsPath": ctx.get(
1663
+ "LEAD_INSTRUCTIONS_RELATIVE_PATH", ""
1664
+ ),
1665
+ "leadExecutionPromptPath": ctx.get(
1666
+ "RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""
1667
+ ),
1668
+ "leadPromptMetadataPath": ctx.get(
1669
+ "LEAD_PROMPT_METADATA_RELATIVE_PATH", ""
1670
+ ),
1671
+ "leadPromptSnapshotPath": ctx.get(
1672
+ "RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""
1673
+ ),
1674
+ "claudeExecutionPromptPath": ctx.get(
1675
+ "RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""
1676
+ ),
1677
+ },
1678
+ "invocationReservationRootPath": ctx.get(
1679
+ "INVOCATION_RESERVATION_ROOT_RELATIVE_PATH", ""
1680
+ ),
1598
1681
  "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1599
1682
  "workerPromptPathByWorkerId": worker_prompt_paths,
1600
1683
  "expectedReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
@@ -1664,14 +1747,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1664
1747
  "disallowLeadSoloAnalysisAsWorkerResult": True,
1665
1748
  "disallowGenericParallelOnlyExecution": True,
1666
1749
  "preferredCompletedWorkerResults": len(reviewers),
1667
- "executor": {
1668
- "provider": ctx.get("EXECUTOR_PROVIDER", ""),
1669
- "displayName": ctx.get("EXECUTOR_DISPLAY_NAME", ""),
1670
- "workerAgent": ctx.get("EXECUTOR_WORKER_AGENT", ""),
1671
- "model": ctx.get("EXECUTOR_MODEL_DISPLAY", ""),
1672
- "modelExecutionValue": ctx.get("EXECUTOR_MODEL_EXECUTION_VALUE", ""),
1673
- "appliesTo": "implementation",
1674
- },
1750
+ "executor": _executor_contract(ctx),
1675
1751
  },
1676
1752
  "validation": {
1677
1753
  "required": True,
@@ -1698,9 +1774,138 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1698
1774
  )
1699
1775
  if _lead_runtime(ctx) == "claude-code":
1700
1776
  payload["claudeSessionId"] = ctx.get("CLAUDE_SESSION_ID", "")
1777
+ _guard_invocation_manifest_rewrite(existing_run_manifest, payload)
1701
1778
  _write_json(Path(run_manifest_path), payload)
1702
1779
 
1703
1780
 
1781
+ _MODEL_ASSIGNMENT_KEYS = {
1782
+ "provider",
1783
+ "model",
1784
+ "modelExecutionValue",
1785
+ "runner",
1786
+ "hostRuntime",
1787
+ "hostModelValue",
1788
+ }
1789
+ _AGENT_CONTRACT_KEYS = {
1790
+ "schemaVersion",
1791
+ "dutyRootPath",
1792
+ "catalogDigest",
1793
+ "invocationReservationRootPath",
1794
+ "allowedAudiences",
1795
+ "authorizedPaths",
1796
+ }
1797
+ _IMMUTABLE_INVOCATION_KEYS = (
1798
+ "agentContract",
1799
+ "invocationAssignments",
1800
+ "leadAssignment",
1801
+ "workerAssignments",
1802
+ "runDirectoryPath",
1803
+ "runManifestPath",
1804
+ "leadInstructionsPath",
1805
+ "leadExecutionPromptPath",
1806
+ "promptSnapshotPath",
1807
+ "leadPromptMetadataPath",
1808
+ "resources",
1809
+ "invocationReservationRootPath",
1810
+ "workerPromptsDirectoryPath",
1811
+ "workerPromptPathByWorkerId",
1812
+ "workerResultsDirectoryPath",
1813
+ "expectedReportPath",
1814
+ "reportTemplatePath",
1815
+ )
1816
+
1817
+
1818
+ def _guard_invocation_manifest_rewrite(
1819
+ existing: Mapping[str, object],
1820
+ proposed: Mapping[str, object],
1821
+ ) -> None:
1822
+ existing_has_contract = bool(existing.get("agentContract"))
1823
+ proposed_has_contract = bool(proposed.get("agentContract"))
1824
+ if existing_has_contract:
1825
+ _validate_invocation_manifest(existing)
1826
+ if proposed_has_contract:
1827
+ _validate_invocation_manifest(proposed)
1828
+ elif existing_has_contract:
1829
+ raise ValueError("incomplete invocation manifest: proposed contract missing")
1830
+ if not existing_has_contract:
1831
+ return
1832
+ drift = [
1833
+ key
1834
+ for key in _IMMUTABLE_INVOCATION_KEYS
1835
+ if existing.get(key) != proposed.get(key)
1836
+ ]
1837
+ if drift:
1838
+ raise ValueError(
1839
+ "immutable invocation manifest fields changed: " + ", ".join(drift)
1840
+ )
1841
+
1842
+
1843
+ def _validate_invocation_manifest(payload: Mapping[str, object]) -> None:
1844
+ contract = payload.get("agentContract")
1845
+ assignments = payload.get("invocationAssignments")
1846
+ if (
1847
+ not isinstance(contract, Mapping)
1848
+ or set(contract) != _AGENT_CONTRACT_KEYS
1849
+ or contract.get("schemaVersion") != 1
1850
+ or not isinstance(assignments, Mapping)
1851
+ or not assignments
1852
+ ):
1853
+ raise ValueError("incomplete invocation manifest")
1854
+ if any(
1855
+ not isinstance(item, Mapping) or set(item) != _MODEL_ASSIGNMENT_KEYS
1856
+ for item in assignments.values()
1857
+ ):
1858
+ raise ValueError("incomplete invocation manifest assignments")
1859
+ _validate_compatibility_projections(payload, assignments)
1860
+ for key in (
1861
+ item
1862
+ for item in _IMMUTABLE_INVOCATION_KEYS[4:]
1863
+ if item != "workerPromptPathByWorkerId"
1864
+ ):
1865
+ if not payload.get(key):
1866
+ raise ValueError(f"incomplete invocation manifest path: {key}")
1867
+ workers = payload.get("workerAssignments")
1868
+ worker_prompt_paths = payload.get("workerPromptPathByWorkerId")
1869
+ expected_worker_ids = {
1870
+ str(worker.get("workerId") or "")
1871
+ for worker in workers
1872
+ if isinstance(worker, Mapping)
1873
+ }
1874
+ if (
1875
+ not isinstance(worker_prompt_paths, Mapping)
1876
+ or set(worker_prompt_paths) != expected_worker_ids
1877
+ or any(
1878
+ not isinstance(path, str) or not path
1879
+ for path in worker_prompt_paths.values()
1880
+ )
1881
+ ):
1882
+ raise ValueError(
1883
+ "incomplete invocation manifest path: workerPromptPathByWorkerId"
1884
+ )
1885
+
1886
+
1887
+ def _validate_compatibility_projections(
1888
+ payload: Mapping[str, object],
1889
+ assignments: Mapping[str, object],
1890
+ ) -> None:
1891
+ lead = payload.get("leadAssignment")
1892
+ workers = payload.get("workerAssignments")
1893
+ if not isinstance(lead, Mapping) or not isinstance(workers, list):
1894
+ raise ValueError("incomplete invocation manifest compatibility projection")
1895
+ if _model_projection(lead) != assignments.get("lead"):
1896
+ raise ValueError("immutable invocation manifest lead projection drift")
1897
+ for worker in workers:
1898
+ if not isinstance(worker, Mapping):
1899
+ raise ValueError("incomplete invocation manifest worker projection")
1900
+ reference = f"initial/{worker.get('workerId', '')}"
1901
+ if _model_projection(worker) != assignments.get(reference):
1902
+ raise ValueError("immutable invocation manifest worker projection drift")
1903
+
1904
+
1905
+ def _model_projection(value: Mapping[str, object]) -> dict[str, object]:
1906
+ return {key: value.get(key) for key in _MODEL_ASSIGNMENT_KEYS}
1907
+
1908
+
1704
1909
  def render_timeline(timeline_path: str, ctx: dict) -> None:
1705
1910
  task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
1706
1911
  task_manifest = {}
@@ -27,6 +27,7 @@ from typing import Any, Callable, Mapping, Sequence
27
27
 
28
28
  from .final_report_paths import final_report_data_path, final_report_markdown_path
29
29
  from .paths import task_dir, task_manifest_file
30
+ from .dispatch_state import DispatchError, link_agent_dispatch_result
30
31
 
31
32
 
32
33
  STEP_CHECK_SOURCE = "check-source"
@@ -371,6 +372,15 @@ def run_finalize(
371
372
  for name, command in commands:
372
373
  if before_step is not None:
373
374
  before_step(name)
375
+ if name == STEP_VALIDATE_RUN:
376
+ try:
377
+ _link_lead_result_for_validation(ctx)
378
+ except (DispatchError, OSError, json.JSONDecodeError) as exc:
379
+ return {
380
+ "ok": False,
381
+ "reason": f"lead result linkage failed: {exc}",
382
+ "steps": steps,
383
+ }
374
384
  result = subprocess.run(
375
385
  command,
376
386
  cwd=ctx.project_root,
@@ -387,6 +397,40 @@ def run_finalize(
387
397
  return {"ok": True, "reason": "", "steps": steps}
388
398
 
389
399
 
400
+ def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
401
+ """Link the lead-authored synthesis artifact before Phase 7 validation."""
402
+ manifest = _load_manifest(ctx.manifest_path)
403
+ contract = manifest.get("agentContract")
404
+ if not isinstance(contract, Mapping) or contract.get("schemaVersion") != 1:
405
+ return
406
+ team_state = _load_manifest(ctx.team_state_path)
407
+ lead_dispatches = [
408
+ row for row in (team_state.get("agentDispatches") or [])
409
+ if isinstance(row, Mapping)
410
+ and row.get("audience") == "lead"
411
+ and row.get("workerId") == "lead"
412
+ ]
413
+ if len(lead_dispatches) != 1:
414
+ return
415
+ convergence_value = string_value(manifest.get("convergenceStatePath"))
416
+ convergence_path = (
417
+ resolve_project_path(ctx.project_root, convergence_value)
418
+ if convergence_value else None
419
+ )
420
+ if convergence_path is not None and convergence_path.is_file():
421
+ result_path = convergence_path
422
+ elif ctx.task_type == "release-handoff" and ctx.data_path.is_file():
423
+ result_path = ctx.data_path
424
+ else:
425
+ return
426
+ link_agent_dispatch_result(
427
+ project_root=ctx.project_root,
428
+ run_manifest_path=ctx.manifest_path,
429
+ dispatch_id=str(lead_dispatches[0]["dispatchId"]),
430
+ result_path=result_path,
431
+ )
432
+
433
+
390
434
  def _load_manifest(path: Path) -> Mapping[str, Any]:
391
435
  try:
392
436
  payload = json.loads(path.read_text(encoding="utf-8"))