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.
- package/README.md +6 -5
- package/docs/architecture/storage-model.md +57 -1
- package/docs/architecture.md +70 -2
- package/docs/cli.md +8 -4
- package/docs/for-ai/skills/okstra-code-review.md +3 -2
- package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
- package/docs/pr-template-usage.md +10 -6
- package/docs/project-structure-overview.md +14 -11
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -5
- package/runtime/agents/workers/report-writer-worker.md +9 -4
- package/runtime/agents/workers/translator-worker.md +6 -4
- package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
- package/runtime/prompts/coding-preflight/clean-code.md +13 -0
- package/runtime/prompts/duties/acceptance-critic.md +24 -0
- package/runtime/prompts/duties/acceptance-verifier.md +24 -0
- package/runtime/prompts/duties/analysis-worker.md +24 -0
- package/runtime/prompts/duties/code-reviewer.md +24 -0
- package/runtime/prompts/duties/common.md +35 -0
- package/runtime/prompts/duties/implementation-executor.md +24 -0
- package/runtime/prompts/duties/implementation-verifier.md +24 -0
- package/runtime/prompts/duties/lead.md +24 -0
- package/runtime/prompts/duties/report-writer.md +24 -0
- package/runtime/prompts/duties/reverification-worker.md +24 -0
- package/runtime/prompts/duties/schedule-verifier.md +24 -0
- package/runtime/prompts/duties/scope-critic.md +24 -0
- package/runtime/prompts/duties/translator.md +24 -0
- package/runtime/prompts/lead/convergence.md +51 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
- package/runtime/prompts/lead/plan-body-verification.md +16 -1
- package/runtime/prompts/lead/report-writer.md +20 -5
- package/runtime/prompts/lead/team-contract.md +13 -13
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
- package/runtime/python/okstra_ctl/context_cost.py +46 -5
- package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
- package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
- package/runtime/python/okstra_ctl/doctor.py +150 -16
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
- package/runtime/python/okstra_ctl/path_hints.py +26 -0
- package/runtime/python/okstra_ctl/paths.py +20 -0
- package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
- package/runtime/python/okstra_ctl/ports/host.py +3 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
- package/runtime/python/okstra_ctl/pr_template.py +3 -6
- package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
- package/runtime/python/okstra_ctl/render.py +217 -12
- package/runtime/python/okstra_ctl/report_finalize.py +44 -0
- package/runtime/python/okstra_ctl/run.py +368 -51
- package/runtime/python/okstra_ctl/session.py +16 -12
- package/runtime/python/okstra_ctl/team.py +11 -11
- package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
- package/runtime/skills/okstra-code-review/SKILL.md +22 -3
- package/runtime/skills/okstra-run/SKILL.md +16 -1
- package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
- package/runtime/templates/implementation-worker-preamble.md +0 -10
- package/runtime/templates/report-writer-prompt-preamble.md +0 -9
- package/runtime/templates/reports/settings.template.json +0 -11
- package/runtime/templates/worker-prompt-preamble.md +0 -10
- package/runtime/validators/lib/fixtures.sh +93 -0
- package/runtime/validators/lib/validate-assets.sh +0 -8
- package/runtime/validators/validate-run.py +182 -0
- package/src/cli-registry.mjs +14 -0
- package/src/commands/execute/agent-prompt.mjs +25 -0
- package/src/commands/execute/codex-dispatch.mjs +6 -63
- package/src/commands/execute/worker-dispatch.mjs +76 -0
- package/src/commands/lifecycle/doctor.mjs +18 -3
- package/src/commands/lifecycle/install.mjs +33 -15
- package/src/commands/lifecycle/uninstall.mjs +4 -3
- package/src/lib/install-assets.mjs +9 -0
- package/runtime/agents/workers/antigravity-worker.md +0 -259
- package/runtime/agents/workers/codex-worker.md +0 -259
- package/runtime/agents/workers/grok-worker.md +0 -259
- package/runtime/agents/workers/kimi-worker.md +0 -259
- package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
- package/runtime/templates/operating-standard.md +0 -22
- package/src/lib/worker-agent-render.mjs +0 -50
- /package/runtime/templates/{prd → pr}/pr-body.template.md +0 -0
|
@@ -22,6 +22,7 @@ import re
|
|
|
22
22
|
import shutil
|
|
23
23
|
import subprocess as _subprocess
|
|
24
24
|
import sys
|
|
25
|
+
import tempfile
|
|
25
26
|
from dataclasses import dataclass, field
|
|
26
27
|
from datetime import datetime, timezone
|
|
27
28
|
from pathlib import Path
|
|
@@ -82,6 +83,18 @@ from .models import (
|
|
|
82
83
|
from .schema_excerpt import build_schema_excerpt
|
|
83
84
|
from .registry.host_registry import default_host_registry
|
|
84
85
|
from .registry.provider_registry import default_provider_registry
|
|
86
|
+
from .ports.host_model import HostModelBindingError, HostModelBindingRequest
|
|
87
|
+
from .agent_invocation import (
|
|
88
|
+
AgentInstruction,
|
|
89
|
+
AgentInstructionSource,
|
|
90
|
+
AgentInvocationRequest,
|
|
91
|
+
AgentModelAssignment,
|
|
92
|
+
AgentInvocationError,
|
|
93
|
+
agent_model_assignment_from_payload,
|
|
94
|
+
digest_duty_catalog,
|
|
95
|
+
prepare_agent_invocation,
|
|
96
|
+
verify_agent_invocation,
|
|
97
|
+
)
|
|
85
98
|
from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
86
99
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
87
100
|
from .render import (
|
|
@@ -1355,15 +1368,24 @@ class RoleAssignment:
|
|
|
1355
1368
|
model_display: str
|
|
1356
1369
|
model_execution_value: str
|
|
1357
1370
|
runner: str
|
|
1371
|
+
host_runtime: str
|
|
1372
|
+
host_model_value: str | None
|
|
1358
1373
|
worker_id: str = ""
|
|
1359
1374
|
|
|
1360
|
-
def
|
|
1361
|
-
|
|
1362
|
-
"role": self.role,
|
|
1375
|
+
def to_model_payload(self) -> dict[str, object]:
|
|
1376
|
+
return {
|
|
1363
1377
|
"provider": self.provider,
|
|
1364
1378
|
"model": self.model_display,
|
|
1365
1379
|
"modelExecutionValue": self.model_execution_value,
|
|
1366
1380
|
"runner": self.runner,
|
|
1381
|
+
"hostRuntime": self.host_runtime,
|
|
1382
|
+
"hostModelValue": self.host_model_value,
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
def to_payload(self) -> dict[str, object]:
|
|
1386
|
+
payload = {
|
|
1387
|
+
"role": self.role,
|
|
1388
|
+
**self.to_model_payload(),
|
|
1367
1389
|
}
|
|
1368
1390
|
if self.worker_id:
|
|
1369
1391
|
payload["workerId"] = self.worker_id
|
|
@@ -1386,15 +1408,13 @@ class _ModelBindings:
|
|
|
1386
1408
|
rw: ModelAssignment
|
|
1387
1409
|
critic_choice: str
|
|
1388
1410
|
critic_model_execution: str
|
|
1389
|
-
executor_provider: str
|
|
1390
1411
|
executor_display_name: str
|
|
1391
|
-
executor_worker_agent: str
|
|
1392
|
-
executor_model_meta: ModelAssignment
|
|
1393
1412
|
codex_worker_execution: str
|
|
1394
1413
|
antigravity_worker_execution: str
|
|
1395
|
-
|
|
1414
|
+
executor_assignment: RoleAssignment
|
|
1396
1415
|
lead_assignment: RoleAssignment
|
|
1397
1416
|
worker_assignments: tuple[RoleAssignment, ...]
|
|
1417
|
+
invocation_assignments: dict[str, dict[str, object]]
|
|
1398
1418
|
|
|
1399
1419
|
|
|
1400
1420
|
def recommended_role_models() -> dict[str, str]:
|
|
@@ -1536,6 +1556,7 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
|
|
|
1536
1556
|
f"(got: {critic_choice!r})"
|
|
1537
1557
|
)
|
|
1538
1558
|
critic_model_execution = ""
|
|
1559
|
+
critic_meta = None
|
|
1539
1560
|
if critic_choice in provider_ids("critic"):
|
|
1540
1561
|
critic_meta = _model_for_role(
|
|
1541
1562
|
critic_choice,
|
|
@@ -1561,7 +1582,6 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
|
|
|
1561
1582
|
)
|
|
1562
1583
|
model_meta = m["providers"][executor_provider]
|
|
1563
1584
|
display_name = f"{provider_spec(executor_provider).display_label} executor"
|
|
1564
|
-
worker_agent = f"{executor_provider}-worker"
|
|
1565
1585
|
lead_provider = m["lead_provider"]
|
|
1566
1586
|
lead_execution = m["lead"].execution
|
|
1567
1587
|
lead_runner = _assignment_runner(inp.lead_runtime, lead_provider, "lead")
|
|
@@ -1569,29 +1589,69 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
|
|
|
1569
1589
|
lead_execution = _normalized_execution(
|
|
1570
1590
|
m["lead"], provider=lead_provider, role="lead",
|
|
1571
1591
|
)
|
|
1592
|
+
lead_assignment = _role_assignment(
|
|
1593
|
+
host_runtime=inp.lead_runtime,
|
|
1594
|
+
role="lead",
|
|
1595
|
+
provider=lead_provider,
|
|
1596
|
+
model=m["lead"],
|
|
1597
|
+
execution=lead_execution,
|
|
1598
|
+
runner_role="lead",
|
|
1599
|
+
)
|
|
1572
1600
|
worker_assignments = _build_worker_assignments(inp, workers, m)
|
|
1601
|
+
critic_assignment = (
|
|
1602
|
+
_role_assignment(
|
|
1603
|
+
host_runtime=inp.lead_runtime,
|
|
1604
|
+
role="critic",
|
|
1605
|
+
provider=critic_choice,
|
|
1606
|
+
model=critic_meta,
|
|
1607
|
+
execution=critic_model_execution,
|
|
1608
|
+
runner_role="critic",
|
|
1609
|
+
)
|
|
1610
|
+
if critic_meta is not None
|
|
1611
|
+
else None
|
|
1612
|
+
)
|
|
1613
|
+
translator_assignment = _role_assignment(
|
|
1614
|
+
host_runtime=inp.lead_runtime,
|
|
1615
|
+
role="translator",
|
|
1616
|
+
provider=m["report_writer_provider"],
|
|
1617
|
+
model=m["rw"],
|
|
1618
|
+
execution=_normalized_execution(
|
|
1619
|
+
m["rw"],
|
|
1620
|
+
provider=m["report_writer_provider"],
|
|
1621
|
+
role="report-writer",
|
|
1622
|
+
),
|
|
1623
|
+
runner_role="report-writer",
|
|
1624
|
+
)
|
|
1625
|
+
executor_assignment = _role_assignment(
|
|
1626
|
+
host_runtime=inp.lead_runtime,
|
|
1627
|
+
role="executor",
|
|
1628
|
+
provider=executor_provider,
|
|
1629
|
+
model=model_meta,
|
|
1630
|
+
execution=_normalized_execution(
|
|
1631
|
+
model_meta, provider=executor_provider, role="executor"
|
|
1632
|
+
),
|
|
1633
|
+
runner_role="executor",
|
|
1634
|
+
worker_id=executor_provider,
|
|
1635
|
+
)
|
|
1636
|
+
invocation_assignments = _build_invocation_assignments(
|
|
1637
|
+
lead_assignment,
|
|
1638
|
+
worker_assignments,
|
|
1639
|
+
critic_assignment,
|
|
1640
|
+
translator_assignment,
|
|
1641
|
+
)
|
|
1573
1642
|
return _ModelBindings(
|
|
1574
1643
|
lead=m["lead"], cw=m["cw"], co=m["co"], ge=m["ge"], rw=m["rw"],
|
|
1575
1644
|
critic_choice=critic_choice,
|
|
1576
1645
|
critic_model_execution=critic_model_execution,
|
|
1577
|
-
executor_provider=executor_provider,
|
|
1578
1646
|
executor_display_name=display_name,
|
|
1579
|
-
executor_worker_agent=worker_agent,
|
|
1580
|
-
executor_model_meta=model_meta,
|
|
1581
1647
|
codex_worker_execution=_worker_execution_value(
|
|
1582
1648
|
m["co"], "codex", "worker", workers),
|
|
1583
1649
|
antigravity_worker_execution=_worker_execution_value(
|
|
1584
1650
|
m["ge"], "antigravity", "worker", workers),
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
lead_assignment=RoleAssignment(
|
|
1588
|
-
role="lead",
|
|
1589
|
-
provider=lead_provider,
|
|
1590
|
-
model_display=m["lead"].display,
|
|
1591
|
-
model_execution_value=lead_execution,
|
|
1592
|
-
runner=lead_runner,
|
|
1593
|
-
),
|
|
1651
|
+
executor_assignment=executor_assignment,
|
|
1652
|
+
lead_assignment=lead_assignment,
|
|
1594
1653
|
worker_assignments=worker_assignments,
|
|
1654
|
+
invocation_assignments=invocation_assignments,
|
|
1595
1655
|
)
|
|
1596
1656
|
|
|
1597
1657
|
|
|
@@ -1609,17 +1669,90 @@ def _build_worker_assignments(
|
|
|
1609
1669
|
execution = _worker_execution_value(
|
|
1610
1670
|
meta, provider, "worker", [provider],
|
|
1611
1671
|
)
|
|
1612
|
-
assignments.append(
|
|
1672
|
+
assignments.append(_role_assignment(
|
|
1673
|
+
host_runtime=inp.lead_runtime,
|
|
1613
1674
|
role=role,
|
|
1614
1675
|
provider=provider,
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1676
|
+
model=meta,
|
|
1677
|
+
execution=execution,
|
|
1678
|
+
runner_role=role,
|
|
1618
1679
|
worker_id=worker_id,
|
|
1619
1680
|
))
|
|
1620
1681
|
return tuple(assignments)
|
|
1621
1682
|
|
|
1622
1683
|
|
|
1684
|
+
def _role_assignment(
|
|
1685
|
+
*,
|
|
1686
|
+
host_runtime: str,
|
|
1687
|
+
role: str,
|
|
1688
|
+
provider: str,
|
|
1689
|
+
model: ModelAssignment,
|
|
1690
|
+
execution: str,
|
|
1691
|
+
runner_role: str,
|
|
1692
|
+
worker_id: str = "",
|
|
1693
|
+
) -> RoleAssignment:
|
|
1694
|
+
runner = _assignment_runner(host_runtime, provider, runner_role)
|
|
1695
|
+
host_model_value = _resolve_host_model_value(
|
|
1696
|
+
host_runtime=host_runtime,
|
|
1697
|
+
provider=provider,
|
|
1698
|
+
model=model,
|
|
1699
|
+
execution=execution,
|
|
1700
|
+
runner=runner,
|
|
1701
|
+
)
|
|
1702
|
+
return RoleAssignment(
|
|
1703
|
+
role=role,
|
|
1704
|
+
provider=provider,
|
|
1705
|
+
model_display=model.display,
|
|
1706
|
+
model_execution_value=execution,
|
|
1707
|
+
runner=runner,
|
|
1708
|
+
host_runtime=host_runtime,
|
|
1709
|
+
host_model_value=host_model_value,
|
|
1710
|
+
worker_id=worker_id,
|
|
1711
|
+
)
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
def _resolve_host_model_value(
|
|
1715
|
+
*,
|
|
1716
|
+
host_runtime: str,
|
|
1717
|
+
provider: str,
|
|
1718
|
+
model: ModelAssignment,
|
|
1719
|
+
execution: str,
|
|
1720
|
+
runner: str,
|
|
1721
|
+
) -> str | None:
|
|
1722
|
+
try:
|
|
1723
|
+
port = default_host_registry().resolve(host_runtime).host_model()
|
|
1724
|
+
return port.resolve(HostModelBindingRequest(
|
|
1725
|
+
host_runtime=host_runtime,
|
|
1726
|
+
provider=provider,
|
|
1727
|
+
model=model.display,
|
|
1728
|
+
model_execution_value=execution,
|
|
1729
|
+
runner=runner,
|
|
1730
|
+
))
|
|
1731
|
+
except (HostModelBindingError, HostNotRegistered) as exc:
|
|
1732
|
+
raise PrepareError(str(exc)) from exc
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def _build_invocation_assignments(
|
|
1736
|
+
lead: RoleAssignment,
|
|
1737
|
+
workers: tuple[RoleAssignment, ...],
|
|
1738
|
+
critic: RoleAssignment | None,
|
|
1739
|
+
translator: RoleAssignment,
|
|
1740
|
+
) -> dict[str, dict[str, object]]:
|
|
1741
|
+
assignments = {"lead": lead.to_model_payload()}
|
|
1742
|
+
for assignment in workers:
|
|
1743
|
+
assignments[f"initial/{assignment.worker_id}"] = (
|
|
1744
|
+
assignment.to_model_payload()
|
|
1745
|
+
)
|
|
1746
|
+
assignments[f"reverify/{assignment.worker_id}"] = (
|
|
1747
|
+
assignment.to_model_payload()
|
|
1748
|
+
)
|
|
1749
|
+
if critic is not None:
|
|
1750
|
+
assignments["critic/scope"] = critic.to_model_payload()
|
|
1751
|
+
assignments["critic/acceptance"] = critic.to_model_payload()
|
|
1752
|
+
assignments["translator"] = translator.to_model_payload()
|
|
1753
|
+
return assignments
|
|
1754
|
+
|
|
1755
|
+
|
|
1623
1756
|
def _normalized_execution(meta: ModelAssignment, provider: str, role: str) -> str:
|
|
1624
1757
|
"""Route a role's catalog execution value through the CLI-identity normalizer
|
|
1625
1758
|
so the manifest carries the dispatch-correct spelling. SSOT for the mapping
|
|
@@ -1853,11 +1986,14 @@ def _write_instruction_set_sources(
|
|
|
1853
1986
|
if inp.task_type == "implementation":
|
|
1854
1987
|
profile_rendered += "\n\n{{DESIGN_PREP_CONTEXT}}\n\n{{FIX_RUN_CONTEXT}}"
|
|
1855
1988
|
profile_tokens = (
|
|
1989
|
+
"EXECUTOR_WORKER_ID",
|
|
1856
1990
|
"EXECUTOR_PROVIDER",
|
|
1857
1991
|
"EXECUTOR_DISPLAY_NAME",
|
|
1858
|
-
"EXECUTOR_WORKER_AGENT",
|
|
1859
1992
|
"EXECUTOR_MODEL_DISPLAY",
|
|
1860
1993
|
"EXECUTOR_MODEL_EXECUTION_VALUE",
|
|
1994
|
+
"EXECUTOR_HOST_MODEL_VALUE",
|
|
1995
|
+
"EXECUTOR_RUNNER",
|
|
1996
|
+
"EXECUTOR_DISPATCH_MODE",
|
|
1861
1997
|
"EXECUTOR_WORKTREE_PATH",
|
|
1862
1998
|
"EXECUTOR_WORKTREE_BRANCH",
|
|
1863
1999
|
"EXECUTOR_WORKTREE_BASE_REF",
|
|
@@ -1980,8 +2116,7 @@ def _render_lead_prompt_and_snapshot(
|
|
|
1980
2116
|
final_report_template: Path,
|
|
1981
2117
|
prompt_template: Path,
|
|
1982
2118
|
) -> str:
|
|
1983
|
-
"""
|
|
1984
|
-
프롬프트를 렌더하고, 프롬프트 스냅샷을 기록한 후 prompt_text 를 돌려준다."""
|
|
2119
|
+
"""Render lead instructions, then publish one verified lead invocation."""
|
|
1985
2120
|
# inject populates ctx with compute + default tokens consumed by the lead
|
|
1986
2121
|
# prompt render below (lead-execution-prompt.md). The final-report
|
|
1987
2122
|
# template render is effectively a copy (Jinja2 `{{ var }}` syntax does
|
|
@@ -2016,14 +2151,48 @@ def _render_lead_prompt_and_snapshot(
|
|
|
2016
2151
|
)
|
|
2017
2152
|
except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
|
|
2018
2153
|
pass
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2154
|
+
instructions_path = Path(ctx["LEAD_INSTRUCTIONS_PATH"])
|
|
2155
|
+
render_template_with_ctx(str(prompt_template), str(instructions_path), ctx)
|
|
2156
|
+
assignment = _agent_model_assignment(
|
|
2157
|
+
json.loads(ctx["INVOCATION_ASSIGNMENTS_JSON"])["lead"]
|
|
2158
|
+
)
|
|
2159
|
+
prepared = prepare_agent_invocation(AgentInvocationRequest(
|
|
2160
|
+
invocation_id=(
|
|
2161
|
+
f"{ctx['TASK_TYPE_SEGMENT']}-{ctx['RUN_PROMPTS_SEQ']}-lead"
|
|
2162
|
+
),
|
|
2163
|
+
worker_id="lead",
|
|
2164
|
+
audience="lead",
|
|
2165
|
+
assignment_ref="lead",
|
|
2166
|
+
purpose=None,
|
|
2167
|
+
assignment=assignment,
|
|
2168
|
+
instruction=AgentInstruction(
|
|
2169
|
+
anchor_lines=(),
|
|
2170
|
+
body=instructions_path.read_text(encoding="utf-8"),
|
|
2171
|
+
source_paths=(
|
|
2172
|
+
AgentInstructionSource(
|
|
2173
|
+
"project", ctx["LEAD_INSTRUCTIONS_RELATIVE_PATH"]
|
|
2174
|
+
),
|
|
2175
|
+
AgentInstructionSource("runtime", "prompts/launch.template.md"),
|
|
2176
|
+
AgentInstructionSource(
|
|
2177
|
+
"runtime", "prompts/lead/okstra-lead-contract.md"
|
|
2178
|
+
),
|
|
2179
|
+
),
|
|
2180
|
+
),
|
|
2181
|
+
project_root=Path(ctx["PROJECT_ROOT"]),
|
|
2182
|
+
run_manifest_path=Path(ctx["RUN_MANIFEST_PATH"]),
|
|
2183
|
+
duty_root=Path(ctx["DUTY_CONTRACT_ROOT"]),
|
|
2184
|
+
prompt_path=Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]),
|
|
2185
|
+
metadata_path=Path(ctx["LEAD_PROMPT_METADATA_PATH"]),
|
|
2186
|
+
dispatch_kind="lead",
|
|
2187
|
+
))
|
|
2188
|
+
return prepared.prompt_path.read_text(encoding="utf-8")
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
def _agent_model_assignment(payload: object) -> AgentModelAssignment:
|
|
2192
|
+
try:
|
|
2193
|
+
return agent_model_assignment_from_payload(payload)
|
|
2194
|
+
except AgentInvocationError as exc:
|
|
2195
|
+
raise PrepareError(str(exc)) from exc
|
|
2027
2196
|
|
|
2028
2197
|
|
|
2029
2198
|
def _persist_run_inputs(
|
|
@@ -2066,7 +2235,7 @@ def _persist_run_inputs(
|
|
|
2066
2235
|
inp.report_writer_provider or "claude",
|
|
2067
2236
|
),
|
|
2068
2237
|
"reportWriterModel": models.rw.display,
|
|
2069
|
-
"executor": models.
|
|
2238
|
+
"executor": models.executor_assignment.provider,
|
|
2070
2239
|
"relatedTasks": inp.related_tasks_raw,
|
|
2071
2240
|
"approvedPlanPath": approved_plan_path,
|
|
2072
2241
|
"clarificationResponsePath": inp.clarification_response_path,
|
|
@@ -2083,6 +2252,19 @@ def _persist_run_inputs(
|
|
|
2083
2252
|
)
|
|
2084
2253
|
|
|
2085
2254
|
|
|
2255
|
+
def _persist_pre_dispatch_run_manifest(ctx: dict) -> None:
|
|
2256
|
+
"""Publish and re-read the complete invocation authority before prompts."""
|
|
2257
|
+
render_run_manifest(ctx["RUN_MANIFEST_PATH"], ctx)
|
|
2258
|
+
manifest = _read_existing_manifest(Path(ctx["RUN_MANIFEST_PATH"]))
|
|
2259
|
+
expected_contract = json.loads(ctx["AGENT_CONTRACT_JSON"])
|
|
2260
|
+
expected_assignments = json.loads(ctx["INVOCATION_ASSIGNMENTS_JSON"])
|
|
2261
|
+
if (
|
|
2262
|
+
manifest.get("agentContract") != expected_contract
|
|
2263
|
+
or manifest.get("invocationAssignments") != expected_assignments
|
|
2264
|
+
):
|
|
2265
|
+
raise PrepareError("pre-dispatch run manifest is incomplete")
|
|
2266
|
+
|
|
2267
|
+
|
|
2086
2268
|
def _read_existing_manifest(manifest_path: Path) -> dict:
|
|
2087
2269
|
if not manifest_path.exists():
|
|
2088
2270
|
return {}
|
|
@@ -2255,6 +2437,98 @@ def _write_bundle_artifacts(
|
|
|
2255
2437
|
ctx["FIX_CYCLE_ID"] = _record_fix_cycle_events(inp, ctx)
|
|
2256
2438
|
|
|
2257
2439
|
|
|
2440
|
+
def _prepare_agent_contract(
|
|
2441
|
+
inp: PrepareInputs,
|
|
2442
|
+
ctx: dict,
|
|
2443
|
+
workspace_root: Path,
|
|
2444
|
+
models: _ModelBindings,
|
|
2445
|
+
) -> None:
|
|
2446
|
+
source = _duty_catalog_source(workspace_root)
|
|
2447
|
+
destination = Path(ctx["DUTY_CONTRACT_ROOT"])
|
|
2448
|
+
_snapshot_duty_catalog(source, destination)
|
|
2449
|
+
digest = digest_duty_catalog(destination)
|
|
2450
|
+
allowed_audiences = _allowed_agent_audiences(inp, models)
|
|
2451
|
+
contract = {
|
|
2452
|
+
"schemaVersion": 1,
|
|
2453
|
+
"dutyRootPath": ctx["DUTY_CONTRACT_ROOT_RELATIVE_PATH"],
|
|
2454
|
+
"catalogDigest": digest,
|
|
2455
|
+
"invocationReservationRootPath": (
|
|
2456
|
+
ctx["INVOCATION_RESERVATION_ROOT_RELATIVE_PATH"]
|
|
2457
|
+
),
|
|
2458
|
+
"allowedAudiences": allowed_audiences,
|
|
2459
|
+
"authorizedPaths": {
|
|
2460
|
+
"instructionRoots": [
|
|
2461
|
+
ctx["RUN_PROMPTS_RELATIVE_PATH"],
|
|
2462
|
+
ctx["RUN_STATE_RELATIVE_PATH"],
|
|
2463
|
+
],
|
|
2464
|
+
"promptRoots": [ctx["RUN_PROMPTS_RELATIVE_PATH"]],
|
|
2465
|
+
"resultRoots": [
|
|
2466
|
+
ctx["WORKER_RESULTS_RELATIVE_PATH"],
|
|
2467
|
+
ctx["RUN_REPORTS_RELATIVE_PATH"],
|
|
2468
|
+
],
|
|
2469
|
+
},
|
|
2470
|
+
}
|
|
2471
|
+
ctx["DUTY_CATALOG_DIGEST"] = digest
|
|
2472
|
+
ctx["AGENT_CONTRACT_JSON"] = json.dumps(contract, ensure_ascii=False)
|
|
2473
|
+
ctx["INVOCATION_ASSIGNMENTS_JSON"] = json.dumps(
|
|
2474
|
+
models.invocation_assignments,
|
|
2475
|
+
ensure_ascii=False,
|
|
2476
|
+
)
|
|
2477
|
+
|
|
2478
|
+
|
|
2479
|
+
def _duty_catalog_source(workspace_root: Path) -> Path:
|
|
2480
|
+
candidates = (
|
|
2481
|
+
workspace_root / "prompts" / "duties",
|
|
2482
|
+
okstra_home() / "prompts" / "duties",
|
|
2483
|
+
)
|
|
2484
|
+
for candidate in candidates:
|
|
2485
|
+
if (candidate / "common.md").is_file():
|
|
2486
|
+
return candidate
|
|
2487
|
+
raise PrepareError("agent duty catalog is missing from the runtime")
|
|
2488
|
+
|
|
2489
|
+
|
|
2490
|
+
def _snapshot_duty_catalog(source: Path, destination: Path) -> None:
|
|
2491
|
+
if destination.exists():
|
|
2492
|
+
if digest_duty_catalog(destination) != digest_duty_catalog(source):
|
|
2493
|
+
raise PrepareError("existing run duty snapshot conflicts with runtime catalog")
|
|
2494
|
+
return
|
|
2495
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
2496
|
+
temp_dir = Path(tempfile.mkdtemp(
|
|
2497
|
+
dir=destination.parent,
|
|
2498
|
+
prefix=f".{destination.name}.",
|
|
2499
|
+
suffix=".tmp",
|
|
2500
|
+
))
|
|
2501
|
+
try:
|
|
2502
|
+
shutil.copytree(source, temp_dir, dirs_exist_ok=True)
|
|
2503
|
+
os.rename(temp_dir, destination)
|
|
2504
|
+
except FileExistsError as exc:
|
|
2505
|
+
if digest_duty_catalog(destination) != digest_duty_catalog(source):
|
|
2506
|
+
raise PrepareError("run duty snapshot publication conflict") from exc
|
|
2507
|
+
finally:
|
|
2508
|
+
if temp_dir.exists():
|
|
2509
|
+
shutil.rmtree(temp_dir)
|
|
2510
|
+
|
|
2511
|
+
|
|
2512
|
+
def _allowed_agent_audiences(
|
|
2513
|
+
inp: PrepareInputs,
|
|
2514
|
+
models: _ModelBindings,
|
|
2515
|
+
) -> list[str]:
|
|
2516
|
+
audiences = {"lead", "translator"}
|
|
2517
|
+
if models.worker_assignments:
|
|
2518
|
+
audiences.add("reverification-worker")
|
|
2519
|
+
if any(item.worker_id == "report-writer" for item in models.worker_assignments):
|
|
2520
|
+
audiences.add("report-writer")
|
|
2521
|
+
if inp.task_type == "implementation":
|
|
2522
|
+
audiences.update({"implementation-executor", "implementation-verifier"})
|
|
2523
|
+
elif inp.task_type == "final-verification":
|
|
2524
|
+
audiences.add("acceptance-verifier")
|
|
2525
|
+
else:
|
|
2526
|
+
audiences.add("analysis-worker")
|
|
2527
|
+
if models.critic_choice not in {"", "off"}:
|
|
2528
|
+
audiences.update({"scope-critic", "acceptance-critic"})
|
|
2529
|
+
return sorted(audiences)
|
|
2530
|
+
|
|
2531
|
+
|
|
2258
2532
|
def _record_run_in_central_index(
|
|
2259
2533
|
inp: PrepareInputs,
|
|
2260
2534
|
ctx: dict,
|
|
@@ -2351,6 +2625,10 @@ def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
|
|
|
2351
2625
|
[assignment.to_payload() for assignment in models.worker_assignments],
|
|
2352
2626
|
ensure_ascii=False,
|
|
2353
2627
|
),
|
|
2628
|
+
"INVOCATION_ASSIGNMENTS_JSON": json.dumps(
|
|
2629
|
+
models.invocation_assignments,
|
|
2630
|
+
ensure_ascii=False,
|
|
2631
|
+
),
|
|
2354
2632
|
"CLAUDE_WORKER_MODEL": models.cw.display,
|
|
2355
2633
|
"CLAUDE_WORKER_MODEL_EXECUTION_VALUE": models.cw.execution,
|
|
2356
2634
|
"CODEX_WORKER_MODEL": models.co.display,
|
|
@@ -2366,11 +2644,22 @@ def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
|
|
|
2366
2644
|
),
|
|
2367
2645
|
"",
|
|
2368
2646
|
),
|
|
2369
|
-
"
|
|
2647
|
+
"EXECUTOR_WORKER_ID": models.executor_assignment.worker_id,
|
|
2648
|
+
"EXECUTOR_PROVIDER": models.executor_assignment.provider,
|
|
2370
2649
|
"EXECUTOR_DISPLAY_NAME": models.executor_display_name,
|
|
2371
|
-
"
|
|
2372
|
-
"
|
|
2373
|
-
|
|
2650
|
+
"EXECUTOR_MODEL_DISPLAY": models.executor_assignment.model_display,
|
|
2651
|
+
"EXECUTOR_MODEL_EXECUTION_VALUE": (
|
|
2652
|
+
models.executor_assignment.model_execution_value
|
|
2653
|
+
),
|
|
2654
|
+
"EXECUTOR_HOST_MODEL_VALUE": (
|
|
2655
|
+
models.executor_assignment.host_model_value or ""
|
|
2656
|
+
),
|
|
2657
|
+
"EXECUTOR_RUNNER": models.executor_assignment.runner,
|
|
2658
|
+
"EXECUTOR_DISPATCH_MODE": (
|
|
2659
|
+
"host-native"
|
|
2660
|
+
if models.executor_assignment.runner == "native-session"
|
|
2661
|
+
else "worker-dispatch"
|
|
2662
|
+
),
|
|
2374
2663
|
"CRITIC_CHOICE": models.critic_choice,
|
|
2375
2664
|
"CRITIC_MODEL_EXECUTION_VALUE": models.critic_model_execution,
|
|
2376
2665
|
}
|
|
@@ -2667,18 +2956,21 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2667
2956
|
_write_bundle_artifacts(
|
|
2668
2957
|
inp, ctx, project_root, lead_runtime, claude_session_id,
|
|
2669
2958
|
)
|
|
2959
|
+
_prepare_agent_contract(inp, ctx, workspace_root, models)
|
|
2670
2960
|
|
|
2671
|
-
# ---- write instruction-set scaffolding
|
|
2961
|
+
# ---- write instruction-set scaffolding ----
|
|
2672
2962
|
instruction_set = _write_instruction_set_sources(
|
|
2673
2963
|
inp, ctx, profile_content, review_material, assets.host_rules_file
|
|
2674
2964
|
)
|
|
2965
|
+
# ---- run-inputs persistence ----
|
|
2966
|
+
_persist_run_inputs(inp, ctx, models, selected_reviewers, brief_relative)
|
|
2967
|
+
_persist_pre_dispatch_run_manifest(ctx)
|
|
2968
|
+
|
|
2969
|
+
# ---- lead prompt publication (manifest is now the external authority) ----
|
|
2675
2970
|
prompt_text = _render_lead_prompt_and_snapshot(
|
|
2676
2971
|
inp, ctx, instruction_set, final_report_template, prompt_template
|
|
2677
2972
|
)
|
|
2678
2973
|
|
|
2679
|
-
# ---- run-inputs persistence ----
|
|
2680
|
-
_persist_run_inputs(inp, ctx, models, selected_reviewers, brief_relative)
|
|
2681
|
-
|
|
2682
2974
|
# ---- final status + manifest/discovery renders ----
|
|
2683
2975
|
_finalize_status_and_render_manifests(
|
|
2684
2976
|
inp, ctx, task_index_template, forbidden_by_phase
|
|
@@ -2997,14 +3289,37 @@ def main(argv: list[str]) -> int:
|
|
|
2997
3289
|
|
|
2998
3290
|
def _lead_launch_payload(ctx: dict[str, object]) -> dict[str, object]:
|
|
2999
3291
|
lead_runtime = str(ctx.get("LEAD_RUNTIME", "claude-code"))
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3292
|
+
project_root = Path(str(ctx["PROJECT_ROOT"]))
|
|
3293
|
+
run_manifest_path = Path(str(ctx["RUN_MANIFEST_PATH"]))
|
|
3294
|
+
try:
|
|
3295
|
+
manifest = json.loads(run_manifest_path.read_text(encoding="utf-8"))
|
|
3296
|
+
resources = manifest["resources"]
|
|
3297
|
+
assignment = agent_model_assignment_from_payload(
|
|
3298
|
+
manifest["invocationAssignments"]["lead"]
|
|
3299
|
+
)
|
|
3300
|
+
prompt_file = project_root / resources["leadExecutionPromptPath"]
|
|
3301
|
+
metadata_path = project_root / resources["leadPromptMetadataPath"]
|
|
3302
|
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
3303
|
+
if metadata["prompt"]["path"] != resources["leadExecutionPromptPath"]:
|
|
3304
|
+
raise KeyError("lead prompt resource does not match metadata")
|
|
3305
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError, AgentInvocationError) as exc:
|
|
3306
|
+
raise PrepareError("lead invocation authority is incomplete") from exc
|
|
3307
|
+
errors = verify_agent_invocation(
|
|
3308
|
+
metadata_path,
|
|
3309
|
+
project_root=project_root,
|
|
3310
|
+
expected_run_manifest_path=run_manifest_path,
|
|
3311
|
+
expected_assignment=assignment,
|
|
3312
|
+
expected_worker_id="lead",
|
|
3313
|
+
expected_assignment_ref="lead",
|
|
3314
|
+
expected_audience="lead",
|
|
3004
3315
|
)
|
|
3316
|
+
if errors:
|
|
3317
|
+
raise PrepareError("lead invocation verification failed: " + "; ".join(errors))
|
|
3318
|
+
lead_provider = assignment.provider
|
|
3319
|
+
launch = lead_launch_spec(lead_provider)
|
|
3005
3320
|
launch_argv = lead_launch_argv(
|
|
3006
3321
|
lead_provider,
|
|
3007
|
-
model=
|
|
3322
|
+
model=assignment.model_execution_value,
|
|
3008
3323
|
session_id=str(ctx["CLAUDE_SESSION_ID"]),
|
|
3009
3324
|
prompt=prompt_file.read_text(encoding="utf-8"),
|
|
3010
3325
|
)
|
|
@@ -3014,9 +3329,11 @@ def _lead_launch_payload(ctx: dict[str, object]) -> dict[str, object]:
|
|
|
3014
3329
|
"leadProvider": lead_provider,
|
|
3015
3330
|
"leadExecutable": launch.executable,
|
|
3016
3331
|
"leadSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
3017
|
-
"leadModelExecutionValue":
|
|
3018
|
-
"projectRoot":
|
|
3332
|
+
"leadModelExecutionValue": assignment.model_execution_value,
|
|
3333
|
+
"projectRoot": str(project_root),
|
|
3019
3334
|
"promptFile": str(prompt_file),
|
|
3335
|
+
"runManifestPath": str(run_manifest_path),
|
|
3336
|
+
"leadPromptMetadataPath": str(metadata_path),
|
|
3020
3337
|
"sandboxWaiverNote": launch.sandbox_waiver_note,
|
|
3021
3338
|
"launchArgv": launch_argv,
|
|
3022
3339
|
"launchRequestArgv": launch_argv[launch_prefix_size:],
|
|
@@ -10,6 +10,8 @@ import os
|
|
|
10
10
|
import uuid
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
+
from .dispatch_state import DispatchError, mutate_team_state
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def generate_claude_session_id() -> str:
|
|
15
17
|
"""UUIDv4 문자열."""
|
|
@@ -105,20 +107,22 @@ def record_observed_lead_session(project_root: Path, team_state_path: Path) -> s
|
|
|
105
107
|
sid = resolve_lead_session_id_for_run(project_root, team_state_path.parent.parent)
|
|
106
108
|
if not sid:
|
|
107
109
|
return ""
|
|
110
|
+
def add_observed_session(state: dict) -> bool:
|
|
111
|
+
lead_ids = state.setdefault("leadSessionIds", [])
|
|
112
|
+
if sid in lead_ids:
|
|
113
|
+
return False
|
|
114
|
+
lead_ids.append(sid)
|
|
115
|
+
team = f"session-{sid[:8]}"
|
|
116
|
+
observed = state.setdefault("observedTeamNames", [])
|
|
117
|
+
if team not in observed:
|
|
118
|
+
observed.append(team)
|
|
119
|
+
return True
|
|
120
|
+
|
|
108
121
|
try:
|
|
109
|
-
|
|
110
|
-
except (
|
|
111
|
-
return ""
|
|
112
|
-
lead_ids = state.setdefault("leadSessionIds", [])
|
|
113
|
-
if sid in lead_ids:
|
|
122
|
+
changed = mutate_team_state(team_state_path, add_observed_session)
|
|
123
|
+
except (DispatchError, OSError):
|
|
114
124
|
return ""
|
|
115
|
-
|
|
116
|
-
team = f"session-{sid[:8]}"
|
|
117
|
-
observed = state.setdefault("observedTeamNames", [])
|
|
118
|
-
if team not in observed:
|
|
119
|
-
observed.append(team)
|
|
120
|
-
team_state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
121
|
-
return sid
|
|
125
|
+
return sid if changed else ""
|
|
122
126
|
|
|
123
127
|
|
|
124
128
|
def write_claude_resume_command_file(
|
|
@@ -19,7 +19,7 @@ from . import tmux
|
|
|
19
19
|
from .adapters.dispatch import provider_worker_wrappers
|
|
20
20
|
from .adapters.dispatch.cmux import dispatch_port_for_terminal_backend
|
|
21
21
|
from .application.dispatch_assignments import dispatch_assignments
|
|
22
|
-
from .dispatch_state import TEARDOWN_BEFORE_TERMINAL_REASON
|
|
22
|
+
from .dispatch_state import TEARDOWN_BEFORE_TERMINAL_REASON, mutate_team_state
|
|
23
23
|
from .dispatch_core import (
|
|
24
24
|
BACKEND_CMUX_PANE,
|
|
25
25
|
BACKEND_TMUX_PANE,
|
|
@@ -253,12 +253,16 @@ def _append_pane(panes: list[dict[str, str]], seen: set[str], pane_id: str, kind
|
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
def _mark_teardown_errors(team_state_path: Path) -> None:
|
|
256
|
-
payload
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
record
|
|
260
|
-
|
|
261
|
-
|
|
256
|
+
def mark(payload: dict[str, Any]) -> bool:
|
|
257
|
+
changed = False
|
|
258
|
+
for record in payload.get("workerDispatches", []):
|
|
259
|
+
if isinstance(record, dict) and record.get("status") not in _TERMINAL_STATUSES:
|
|
260
|
+
record["status"] = "error"
|
|
261
|
+
record["reason"] = TEARDOWN_BEFORE_TERMINAL_REASON
|
|
262
|
+
changed = True
|
|
263
|
+
return changed
|
|
264
|
+
|
|
265
|
+
mutate_team_state(team_state_path, mark)
|
|
262
266
|
|
|
263
267
|
|
|
264
268
|
def _emit_teardown(as_json: bool, panes: list[dict[str, str]]) -> None:
|
|
@@ -310,10 +314,6 @@ def _load_json(path: Path, label: str) -> dict[str, Any]:
|
|
|
310
314
|
return payload
|
|
311
315
|
|
|
312
316
|
|
|
313
|
-
def _write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
|
314
|
-
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
315
|
-
|
|
316
|
-
|
|
317
317
|
def _resolve_project_path(project_root: Path, value: str | Path) -> Path:
|
|
318
318
|
path = Path(value)
|
|
319
319
|
return path if path.is_absolute() else project_root / path
|