okstra 0.163.2 → 0.165.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 +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +42 -7
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -1050,11 +1050,16 @@ class UserResponseEntry:
|
|
|
1050
1050
|
|
|
1051
1051
|
|
|
1052
1052
|
@dataclass(frozen=True)
|
|
1053
|
-
class
|
|
1054
|
-
"""HTML
|
|
1055
|
-
|
|
1056
|
-
|
|
1053
|
+
class UserPlanDecision:
|
|
1054
|
+
"""HTML 계획 결정 위젯의 Export 결과.
|
|
1055
|
+
|
|
1056
|
+
``status`` 가 승인이 아니면 ``reason`` 이 필수다 — 사유 없는 반려는 다음
|
|
1057
|
+
run 이 무엇을 고쳐야 하는지 알 수 없어 되돌아올 수밖에 없다.
|
|
1058
|
+
``implementation_option`` 이 빈 문자열이면 라인을 생략한다 (소비 측은
|
|
1059
|
+
Recommended Option 폴백)."""
|
|
1060
|
+
status: str
|
|
1057
1061
|
implementation_option: str = ""
|
|
1062
|
+
reason: str = ""
|
|
1058
1063
|
|
|
1059
1064
|
|
|
1060
1065
|
@dataclass(frozen=True)
|
|
@@ -1072,6 +1077,13 @@ _ANALYSIS_REVIEW_STATUSES = frozenset({
|
|
|
1072
1077
|
"rejected",
|
|
1073
1078
|
})
|
|
1074
1079
|
|
|
1080
|
+
PLAN_DECISION_APPROVED = "approved"
|
|
1081
|
+
_PLAN_DECISION_STATUSES = frozenset({
|
|
1082
|
+
PLAN_DECISION_APPROVED,
|
|
1083
|
+
"revision-requested",
|
|
1084
|
+
"rejected",
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1075
1087
|
|
|
1076
1088
|
def _quoted_sidecar_field(label: str, value: str) -> str:
|
|
1077
1089
|
cleaned = value.strip()
|
|
@@ -1100,12 +1112,25 @@ def _serialize_analysis_review(review: UserResponseAnalysisReview) -> str:
|
|
|
1100
1112
|
)
|
|
1101
1113
|
|
|
1102
1114
|
|
|
1115
|
+
def _serialize_plan_decision(decision: UserPlanDecision) -> str:
|
|
1116
|
+
if decision.status not in _PLAN_DECISION_STATUSES:
|
|
1117
|
+
raise ValueError(f"invalid PLAN DECISION status: {decision.status}")
|
|
1118
|
+
if decision.status != PLAN_DECISION_APPROVED and not decision.reason.strip():
|
|
1119
|
+
raise ValueError(f"PLAN DECISION {decision.status} requires a Reason")
|
|
1120
|
+
chunk = f"\n## PLAN DECISION\n- Status: {decision.status}\n"
|
|
1121
|
+
if decision.implementation_option:
|
|
1122
|
+
chunk += f"- Implementation-Option: {decision.implementation_option.strip()}\n"
|
|
1123
|
+
if decision.reason.strip():
|
|
1124
|
+
chunk += _quoted_sidecar_field("Reason", decision.reason)
|
|
1125
|
+
return chunk
|
|
1126
|
+
|
|
1127
|
+
|
|
1103
1128
|
def serialize_user_response(
|
|
1104
1129
|
*,
|
|
1105
1130
|
run_meta: RunMeta,
|
|
1106
1131
|
entries: list[UserResponseEntry],
|
|
1107
1132
|
created_at: str,
|
|
1108
|
-
|
|
1133
|
+
plan_decision: UserPlanDecision | None = None,
|
|
1109
1134
|
analysis_review: UserResponseAnalysisReview | None = None,
|
|
1110
1135
|
) -> str:
|
|
1111
1136
|
"""Return the canonical markdown text the HTML 'Export user
|
|
@@ -1125,7 +1150,7 @@ def serialize_user_response(
|
|
|
1125
1150
|
"\n"
|
|
1126
1151
|
"# User Response\n"
|
|
1127
1152
|
)
|
|
1128
|
-
|
|
1153
|
+
has_plan_decision = plan_decision is not None
|
|
1129
1154
|
has_analysis_review = analysis_review is not None
|
|
1130
1155
|
body_chunks: list[str] = []
|
|
1131
1156
|
for e in entries:
|
|
@@ -1137,13 +1162,10 @@ def serialize_user_response(
|
|
|
1137
1162
|
if e.rationale:
|
|
1138
1163
|
chunk += f"- Rationale: {e.rationale.strip()}\n"
|
|
1139
1164
|
body_chunks.append(chunk)
|
|
1140
|
-
if not entries and not
|
|
1165
|
+
if not entries and not has_plan_decision and not has_analysis_review:
|
|
1141
1166
|
body_chunks.append("\n_(No user responses recorded.)_\n")
|
|
1142
|
-
if
|
|
1143
|
-
|
|
1144
|
-
if approval.implementation_option:
|
|
1145
|
-
chunk += f"- Implementation-Option: {approval.implementation_option.strip()}\n"
|
|
1146
|
-
body_chunks.append(chunk)
|
|
1167
|
+
if plan_decision is not None:
|
|
1168
|
+
body_chunks.append(_serialize_plan_decision(plan_decision))
|
|
1147
1169
|
if analysis_review is not None:
|
|
1148
1170
|
body_chunks.append(_serialize_analysis_review(analysis_review))
|
|
1149
1171
|
return head + "".join(body_chunks)
|
|
@@ -1369,11 +1391,17 @@ def _plan_approval_section(ctx: PlanApprovalContext, run_meta: RunMeta) -> str:
|
|
|
1369
1391
|
)
|
|
1370
1392
|
return (
|
|
1371
1393
|
'<section id="plan-approval">\n'
|
|
1372
|
-
" <h2>Plan
|
|
1394
|
+
" <h2>Plan Decision</h2>\n"
|
|
1373
1395
|
f' <label>구현 옵션: <select id="approval-option"{disabled}>{"".join(opts)}</select></label>\n'
|
|
1374
|
-
|
|
1375
|
-
"
|
|
1376
|
-
"
|
|
1396
|
+
' <fieldset id="plan-decision"><legend>판정</legend>'
|
|
1397
|
+
f'<label><input type="radio" name="plan-decision-status" value="approved"{disabled}> '
|
|
1398
|
+
"이 plan 을 승인합니다</label>"
|
|
1399
|
+
'<label><input type="radio" name="plan-decision-status" '
|
|
1400
|
+
'value="revision-requested"> 고쳐서 다시 가져오게 합니다</label>'
|
|
1401
|
+
'<label><input type="radio" name="plan-decision-status" value="rejected"> '
|
|
1402
|
+
"이 plan 을 반려합니다</label></fieldset>\n"
|
|
1403
|
+
' <label>사유 — 반려하거나 다시 고치게 할 때는 반드시 적어야 합니다'
|
|
1404
|
+
'<textarea id="plan-decision-reason" rows="4"></textarea></label>'
|
|
1377
1405
|
f"{reason_html}\n"
|
|
1378
1406
|
"</section>\n"
|
|
1379
1407
|
)
|
|
@@ -57,14 +57,19 @@ from .material import (
|
|
|
57
57
|
from .final_report_schema import load_schema_version
|
|
58
58
|
from .final_report_paths import final_report_data_path as _final_report_data_path
|
|
59
59
|
from .lead_events import LeadEvent, append_lead_event
|
|
60
|
-
from .
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
from .application.resolve_assignment import (
|
|
61
|
+
resolve_assignment_runner,
|
|
62
|
+
resolve_lead_provider,
|
|
63
|
+
)
|
|
64
|
+
from .domain.host import (
|
|
65
|
+
HostCapabilityMismatch,
|
|
66
|
+
HostNotRegistered,
|
|
67
|
+
ProviderUnavailable,
|
|
64
68
|
)
|
|
65
69
|
from .model_discovery import normalize_execution_for_dispatch
|
|
66
70
|
from .models import (
|
|
67
71
|
ModelAssignment,
|
|
72
|
+
UnknownProviderError,
|
|
68
73
|
default_model,
|
|
69
74
|
lead_launch_argv,
|
|
70
75
|
lead_launch_spec,
|
|
@@ -75,11 +80,8 @@ from .models import (
|
|
|
75
80
|
resolve_model_metadata,
|
|
76
81
|
)
|
|
77
82
|
from .schema_excerpt import build_schema_excerpt
|
|
78
|
-
from .
|
|
79
|
-
|
|
80
|
-
native_provider_for_host,
|
|
81
|
-
resolve_runner,
|
|
82
|
-
)
|
|
83
|
+
from .registry.host_registry import default_host_registry
|
|
84
|
+
from .registry.provider_registry import default_provider_registry
|
|
83
85
|
from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
84
86
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
85
87
|
from .render import (
|
|
@@ -326,15 +328,16 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
|
326
328
|
PrepareError = _stage_targets.PrepareError
|
|
327
329
|
|
|
328
330
|
|
|
329
|
-
_ALLOWED_LEAD_RUNTIMES = ALLOWED_LEAD_RUNTIMES
|
|
330
|
-
|
|
331
|
-
|
|
332
331
|
def _normalize_lead_runtime(value: str) -> str:
|
|
333
332
|
runtime = (value or "claude-code").strip()
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
333
|
+
registry = default_host_registry()
|
|
334
|
+
try:
|
|
335
|
+
return registry.resolve(runtime).descriptor.id
|
|
336
|
+
except HostNotRegistered as exc:
|
|
337
|
+
allowed = ", ".join(registry.ids())
|
|
338
|
+
raise PrepareError(
|
|
339
|
+
f"unsupported lead runtime: {runtime} (allowed: {allowed})"
|
|
340
|
+
) from exc
|
|
338
341
|
|
|
339
342
|
|
|
340
343
|
@dataclass
|
|
@@ -726,7 +729,8 @@ def _brief_sha256(path: Path) -> str:
|
|
|
726
729
|
|
|
727
730
|
|
|
728
731
|
def _record_artifact_runtime_render_only_event(inp: PrepareInputs, ctx: dict) -> None:
|
|
729
|
-
|
|
732
|
+
descriptor = default_host_registry().resolve(inp.lead_runtime).descriptor
|
|
733
|
+
if descriptor.session_accounting != "artifact-only" or not inp.render_only:
|
|
730
734
|
return
|
|
731
735
|
append_lead_event(
|
|
732
736
|
Path(ctx["LEAD_EVENTS_PATH"]),
|
|
@@ -1446,9 +1450,20 @@ def _model_for_role(
|
|
|
1446
1450
|
|
|
1447
1451
|
|
|
1448
1452
|
def _assignment_runner(lead_runtime: str, provider: str, role: str) -> str:
|
|
1453
|
+
host_registry = default_host_registry()
|
|
1454
|
+
provider_registry = default_provider_registry()
|
|
1449
1455
|
try:
|
|
1450
|
-
return
|
|
1451
|
-
|
|
1456
|
+
return resolve_assignment_runner(
|
|
1457
|
+
host=host_registry.resolve(lead_runtime).descriptor,
|
|
1458
|
+
provider=provider_registry.resolve(provider),
|
|
1459
|
+
role=role,
|
|
1460
|
+
).runner
|
|
1461
|
+
except (
|
|
1462
|
+
HostCapabilityMismatch,
|
|
1463
|
+
HostNotRegistered,
|
|
1464
|
+
ProviderUnavailable,
|
|
1465
|
+
UnknownProviderError,
|
|
1466
|
+
) as exc:
|
|
1452
1467
|
raise PrepareError(str(exc)) from exc
|
|
1453
1468
|
|
|
1454
1469
|
|
|
@@ -1468,23 +1483,22 @@ def _resolve_worker_models(inp: PrepareInputs) -> dict:
|
|
|
1468
1483
|
provider, overrides.get(provider, legacy.get(provider, "")),
|
|
1469
1484
|
"analyser", default_override,
|
|
1470
1485
|
)
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1486
|
+
try:
|
|
1487
|
+
lead_assignment = resolve_lead_provider(
|
|
1488
|
+
host_id=inp.lead_runtime,
|
|
1489
|
+
requested_provider=inp.lead_provider,
|
|
1490
|
+
host_registry=default_host_registry(),
|
|
1491
|
+
provider_registry=default_provider_registry(),
|
|
1492
|
+
)
|
|
1493
|
+
except (
|
|
1494
|
+
HostCapabilityMismatch,
|
|
1495
|
+
HostNotRegistered,
|
|
1496
|
+
ProviderUnavailable,
|
|
1497
|
+
UnknownProviderError,
|
|
1498
|
+
) as exc:
|
|
1499
|
+
raise PrepareError(str(exc)) from exc
|
|
1500
|
+
lead_provider = lead_assignment.provider
|
|
1485
1501
|
report_writer_provider = inp.report_writer_provider.strip().lower() or "claude"
|
|
1486
|
-
if not provider_supports_role(lead_provider, "lead"):
|
|
1487
|
-
raise PrepareError(f"provider {lead_provider!r} does not support the lead role")
|
|
1488
1502
|
if not provider_supports_role(report_writer_provider, "report-writer"):
|
|
1489
1503
|
raise PrepareError(
|
|
1490
1504
|
f"provider {report_writer_provider!r} does not support the report-writer role"
|
|
@@ -2736,7 +2750,7 @@ def main(argv: list[str]) -> int:
|
|
|
2736
2750
|
p.add_argument(
|
|
2737
2751
|
"--lead-runtime",
|
|
2738
2752
|
default="claude-code",
|
|
2739
|
-
choices=list(
|
|
2753
|
+
choices=list(default_host_registry().ids()),
|
|
2740
2754
|
dest="lead_runtime",
|
|
2741
2755
|
help=(
|
|
2742
2756
|
"Lead runtime adapter. Default: claude-code. `codex` renders "
|
|
@@ -2964,8 +2978,7 @@ def main(argv: list[str]) -> int:
|
|
|
2964
2978
|
else:
|
|
2965
2979
|
print(f"okstra current run dir: {ctx['RUN_DIR']}")
|
|
2966
2980
|
print(f"final report path: {ctx['FINAL_REPORT_PATH']}")
|
|
2967
|
-
|
|
2968
|
-
lead_provider = lead_runtime_info(lead_runtime_name).agent
|
|
2981
|
+
lead_provider = ctx.get("LEAD_PROVIDER", "")
|
|
2969
2982
|
launch = lead_launch_spec(lead_provider)
|
|
2970
2983
|
print(f"lead model: {ctx['LEAD_MODEL']}")
|
|
2971
2984
|
print(f"claude session id: {ctx['CLAUDE_SESSION_ID']}")
|
|
@@ -2977,30 +2990,39 @@ def main(argv: list[str]) -> int:
|
|
|
2977
2990
|
# lead. The argv is assembled here rather than in the caller so provider
|
|
2978
2991
|
# launch knowledge stays in the catalog and every front end — the bash
|
|
2979
2992
|
# wrapper and the Node CLI — starts the lead identically.
|
|
2980
|
-
|
|
2981
|
-
machine = {
|
|
2982
|
-
"leadRuntime": lead_runtime_name,
|
|
2983
|
-
"leadProvider": lead_provider,
|
|
2984
|
-
"leadExecutable": launch.executable,
|
|
2985
|
-
"leadSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
2986
|
-
"leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
2987
|
-
"projectRoot": ctx["PROJECT_ROOT"],
|
|
2988
|
-
"promptFile": str(prompt_file),
|
|
2989
|
-
# Non-empty only when starting this lead lowers a protection the
|
|
2990
|
-
# user should agree to first. The front end asks; okstra does not
|
|
2991
|
-
# waive it silently.
|
|
2992
|
-
"sandboxWaiverNote": launch.sandbox_waiver_note,
|
|
2993
|
-
"launchArgv": lead_launch_argv(
|
|
2994
|
-
lead_provider,
|
|
2995
|
-
model=ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
2996
|
-
session_id=ctx["CLAUDE_SESSION_ID"],
|
|
2997
|
-
prompt=prompt_file.read_text(encoding="utf-8"),
|
|
2998
|
-
),
|
|
2999
|
-
}
|
|
2993
|
+
machine = _lead_launch_payload(ctx)
|
|
3000
2994
|
print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
|
|
3001
2995
|
return 0
|
|
3002
2996
|
|
|
3003
2997
|
|
|
2998
|
+
def _lead_launch_payload(ctx: dict[str, object]) -> dict[str, object]:
|
|
2999
|
+
lead_runtime = str(ctx.get("LEAD_RUNTIME", "claude-code"))
|
|
3000
|
+
lead_provider = str(ctx.get("LEAD_PROVIDER", ""))
|
|
3001
|
+
launch = lead_launch_spec(lead_provider)
|
|
3002
|
+
prompt_file = (
|
|
3003
|
+
Path(str(ctx["INSTRUCTION_SET_PATH"])) / "lead-execution-prompt.md"
|
|
3004
|
+
)
|
|
3005
|
+
launch_argv = lead_launch_argv(
|
|
3006
|
+
lead_provider,
|
|
3007
|
+
model=str(ctx["LEAD_MODEL_EXECUTION_VALUE"]),
|
|
3008
|
+
session_id=str(ctx["CLAUDE_SESSION_ID"]),
|
|
3009
|
+
prompt=prompt_file.read_text(encoding="utf-8"),
|
|
3010
|
+
)
|
|
3011
|
+
launch_prefix_size = 1 + len(launch.sandbox_waiver)
|
|
3012
|
+
return {
|
|
3013
|
+
"leadRuntime": lead_runtime,
|
|
3014
|
+
"leadProvider": lead_provider,
|
|
3015
|
+
"leadExecutable": launch.executable,
|
|
3016
|
+
"leadSessionId": ctx["CLAUDE_SESSION_ID"],
|
|
3017
|
+
"leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
|
|
3018
|
+
"projectRoot": ctx["PROJECT_ROOT"],
|
|
3019
|
+
"promptFile": str(prompt_file),
|
|
3020
|
+
"sandboxWaiverNote": launch.sandbox_waiver_note,
|
|
3021
|
+
"launchArgv": launch_argv,
|
|
3022
|
+
"launchRequestArgv": launch_argv[launch_prefix_size:],
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
|
|
3004
3026
|
if __name__ == "__main__":
|
|
3005
3027
|
import sys
|
|
3006
3028
|
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -186,7 +186,7 @@ cat >&2 <<EOF
|
|
|
186
186
|
EOF
|
|
187
187
|
|
|
188
188
|
cd "$PROJECT_ROOT"
|
|
189
|
-
exec
|
|
189
|
+
exec "$HOME/.okstra/bin/okstra.sh" --resume-session "$SESSION_ID" --lead-runtime claude-code --project-root "$PROJECT_ROOT"
|
|
190
190
|
"""
|
|
191
191
|
resume_command_path.write_text(body, encoding="utf-8")
|
|
192
192
|
os.chmod(resume_command_path, 0o755)
|
|
@@ -9,6 +9,7 @@ which decides whether a re-run narrows or stays full.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import re
|
|
12
|
+
from collections.abc import Iterator
|
|
12
13
|
|
|
13
14
|
_RANGE = r"(?:[-–]|\bto\b|\bthrough\b)"
|
|
14
15
|
_ITEM = rf"\d+(?:\s*{_RANGE}\s*\d+)?"
|
|
@@ -23,25 +24,61 @@ _ITEM_RE = re.compile(rf"(\d+)(?:\s*{_RANGE}\s*(\d+))?", re.IGNORECASE)
|
|
|
23
24
|
RANGE_MAX_SPAN = 64
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _stage_citation_items(text: str) -> Iterator[tuple[int, int | None]]:
|
|
28
|
+
"""Each `stage`-anchored citation in *text* as `(start, end)`.
|
|
29
|
+
|
|
30
|
+
*end* is None for a bare number and the far endpoint for a range. Numbers
|
|
31
|
+
stay anchored to a word-initial `stage`/`stages` token on the same line;
|
|
32
|
+
harvesting bare numbers — or letting the anchor reach across a line break,
|
|
33
|
+
or match the tail of `Substage`/`Backstage` — would let any prose, including
|
|
34
|
+
a row disclaiming every stage, justify any stage.
|
|
35
|
+
"""
|
|
36
|
+
for span in _LIST_RE.finditer(text):
|
|
37
|
+
for item in _ITEM_RE.finditer(span.group(1)):
|
|
38
|
+
end = item.group(2)
|
|
39
|
+
yield int(item.group(1)), int(end) if end is not None else None
|
|
40
|
+
|
|
41
|
+
|
|
26
42
|
def cited_stage_numbers(text: str) -> set[int]:
|
|
27
43
|
"""Stage numbers *text* cites, in every prose form a planner writes.
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
break, or match the tail of `Substage`/`Backstage` — would let any prose,
|
|
32
|
-
including a row disclaiming every stage, justify any stage.
|
|
45
|
+
A range reaches every number between its endpoints here, which is what a
|
|
46
|
+
reader asking "could this touch stage 5" needs.
|
|
33
47
|
"""
|
|
34
48
|
cited: set[int] = set()
|
|
49
|
+
for start, end in _stage_citation_items(text):
|
|
50
|
+
cited.add(start)
|
|
51
|
+
if end is None:
|
|
52
|
+
continue
|
|
53
|
+
cited.add(end)
|
|
54
|
+
# A reversed or absurdly wide range is a typo, not a citation of
|
|
55
|
+
# everything between its endpoints.
|
|
56
|
+
if 0 <= end - start <= RANGE_MAX_SPAN:
|
|
57
|
+
cited.update(range(start, end))
|
|
58
|
+
return cited
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def enumerated_stage_numbers(text: str) -> set[int]:
|
|
62
|
+
"""Stage numbers *text* names one by one — a range's interior excluded.
|
|
63
|
+
|
|
64
|
+
The two readers of this grammar ask opposite questions, and a range answers
|
|
65
|
+
only one of them. The incremental-scope back-trace asks "could this answer
|
|
66
|
+
reach stage 5", so it must read `Stages 1-8` as reaching it — that is
|
|
67
|
+
`cited_stage_numbers`, and widening there is the safe direction. Coverage
|
|
68
|
+
provenance asks "did the planner confirm stage 5 satisfies this
|
|
69
|
+
requirement", and a range answers that for free: one `Stages 1-64` cell
|
|
70
|
+
stamps every stage in the map without the planner looking at any of them.
|
|
71
|
+
|
|
72
|
+
So the interior is dropped here and only hand-written numbers count. A
|
|
73
|
+
range's endpoints ARE hand-written and stay, which keeps `Stages 7-8`
|
|
74
|
+
honest while forcing the wide case to be spelled out. The cost of a
|
|
75
|
+
legitimately broad requirement is typing each number, and that typing is
|
|
76
|
+
the confirmation this check is asking for.
|
|
77
|
+
"""
|
|
78
|
+
enumerated: set[int] = set()
|
|
35
79
|
for span in _LIST_RE.finditer(text):
|
|
36
80
|
for item in _ITEM_RE.finditer(span.group(1)):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end = int(item.group(2))
|
|
42
|
-
cited.add(end)
|
|
43
|
-
# A reversed or absurdly wide range is a typo, not a citation of
|
|
44
|
-
# everything between its endpoints.
|
|
45
|
-
if 0 <= end - start <= RANGE_MAX_SPAN:
|
|
46
|
-
cited.update(range(start, end))
|
|
47
|
-
return cited
|
|
81
|
+
enumerated.add(int(item.group(1)))
|
|
82
|
+
if item.group(2) is not None:
|
|
83
|
+
enumerated.add(int(item.group(2)))
|
|
84
|
+
return enumerated
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Neutral okstra team CLI for pane-backed worker dispatch.
|
|
2
2
|
|
|
3
|
-
Outside cmux this is the
|
|
4
|
-
is every lead's door onto cmux surfaces,
|
|
5
|
-
rather than the host. Which backend a run
|
|
3
|
+
Outside cmux this is the door onto tmux panes for hosts whose descriptor owns
|
|
4
|
+
the team lifecycle. Under cmux it is every lead's door onto cmux surfaces,
|
|
5
|
+
because okstra owns the panes there rather than the host. Which backend a run
|
|
6
|
+
uses is read from its run manifest.
|
|
6
7
|
"""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
@@ -14,18 +15,23 @@ from typing import Any, Mapping, Sequence
|
|
|
14
15
|
|
|
15
16
|
from . import cmux
|
|
16
17
|
from . import tmux
|
|
18
|
+
from .adapters.dispatch import provider_worker_wrappers
|
|
19
|
+
from .adapters.dispatch.cmux import dispatch_port_for_terminal_backend
|
|
20
|
+
from .application.dispatch_assignments import dispatch_assignments
|
|
17
21
|
from .dispatch_core import (
|
|
18
22
|
BACKEND_CMUX_PANE,
|
|
19
23
|
BACKEND_TMUX_PANE,
|
|
20
24
|
DispatchError,
|
|
21
25
|
DispatchPlan,
|
|
22
26
|
await_dispatches,
|
|
23
|
-
build_dispatch_plan,
|
|
24
27
|
dispatch_plan,
|
|
25
28
|
)
|
|
26
|
-
from .
|
|
29
|
+
from .ports.worker_dispatch import WorkerDispatchRequest
|
|
30
|
+
from .registry.host_registry import default_host_registry
|
|
31
|
+
from .registry.provider_registry import default_provider_registry
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
|
|
34
|
+
_SUPPORTED_WRAPPERS = provider_worker_wrappers(default_provider_registry())
|
|
29
35
|
_TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
30
36
|
|
|
31
37
|
|
|
@@ -92,33 +98,43 @@ def _dispatch(args) -> int:
|
|
|
92
98
|
if args.jobs_file and args.workers:
|
|
93
99
|
raise DispatchError("--jobs-file and --workers cannot be combined")
|
|
94
100
|
manifest = _load_manifest(args.project_root, args.run_manifest)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
terminal_backend = _manifest_backend(manifest)
|
|
102
|
+
lead_runtime = _require_string(manifest, "leadRuntime")
|
|
103
|
+
fallback_port = default_host_registry().resolve(
|
|
104
|
+
lead_runtime
|
|
105
|
+
).worker_dispatch()
|
|
106
|
+
port = dispatch_port_for_terminal_backend(
|
|
107
|
+
terminal_backend,
|
|
108
|
+
fallback_port,
|
|
109
|
+
supported_worker_wrappers=_SUPPORTED_WRAPPERS,
|
|
110
|
+
unsupported_worker_label=f"{lead_runtime} lead",
|
|
111
|
+
required_lead_runtime=None if _is_cmux_run(manifest) else lead_runtime,
|
|
112
|
+
)
|
|
113
|
+
request = WorkerDispatchRequest(
|
|
98
114
|
project_root=Path(args.project_root),
|
|
99
|
-
|
|
115
|
+
run_manifest=Path(args.run_manifest),
|
|
100
116
|
workspace_root=Path(args.workspace_root),
|
|
101
117
|
okstra_bin=Path(args.okstra_bin),
|
|
102
|
-
requested_workers=
|
|
118
|
+
requested_workers=tuple(_parse_workers(args.workers)),
|
|
103
119
|
idle_timeout_seconds=args.idle_timeout_seconds,
|
|
104
|
-
required_lead_runtime=None if _is_cmux_run(manifest) else "external",
|
|
105
|
-
default_backend=_manifest_backend(manifest),
|
|
106
|
-
supported_worker_wrappers=_SUPPORTED_WRAPPERS,
|
|
107
|
-
unsupported_worker_label="external lead",
|
|
108
120
|
dispatch_kind=args.dispatch_kind,
|
|
109
121
|
jobs_file=Path(args.jobs_file) if args.jobs_file else None,
|
|
110
122
|
)
|
|
123
|
+
plan = dispatch_assignments(request, port)
|
|
124
|
+
backend_plan = plan.backend_plan
|
|
125
|
+
if backend_plan is None:
|
|
126
|
+
raise DispatchError("team dispatch did not produce a backend plan")
|
|
111
127
|
if args.dry_run:
|
|
112
|
-
_print_json(
|
|
128
|
+
_print_json(backend_plan.to_payload(dry_run=True))
|
|
113
129
|
return 0
|
|
114
|
-
result = dispatch_plan(
|
|
115
|
-
_print_json(
|
|
130
|
+
result = dispatch_plan(backend_plan, wait=False)
|
|
131
|
+
_print_json(backend_plan.to_payload(dry_run=False))
|
|
116
132
|
return result
|
|
117
133
|
|
|
118
134
|
|
|
119
135
|
def _await(args) -> int:
|
|
120
136
|
manifest = _load_manifest(args.project_root, args.run_manifest)
|
|
121
|
-
|
|
137
|
+
_validate_team_manifest(manifest)
|
|
122
138
|
plan = _plan_for_existing(Path(args.project_root), Path(args.workspace_root), Path(args.run_manifest), manifest)
|
|
123
139
|
code = await_dispatches(
|
|
124
140
|
plan,
|
|
@@ -135,7 +151,7 @@ def _await(args) -> int:
|
|
|
135
151
|
|
|
136
152
|
def _teardown(args) -> int:
|
|
137
153
|
manifest = _load_manifest(args.project_root, args.run_manifest)
|
|
138
|
-
|
|
154
|
+
_validate_team_manifest(manifest)
|
|
139
155
|
project_root = Path(args.project_root).resolve()
|
|
140
156
|
team_state_path = _resolve_project_path(project_root, _require_string(manifest, "teamStatePath"))
|
|
141
157
|
team_state = _load_json(team_state_path, "team-state")
|
|
@@ -242,20 +258,16 @@ def _is_cmux_run(manifest: Mapping[str, Any]) -> bool:
|
|
|
242
258
|
return _manifest_backend(manifest) == BACKEND_CMUX_PANE
|
|
243
259
|
|
|
244
260
|
|
|
245
|
-
def
|
|
246
|
-
runtime = manifest.get("leadRuntime")
|
|
247
|
-
if runtime == "external":
|
|
248
|
-
return
|
|
261
|
+
def _validate_team_manifest(manifest: Mapping[str, Any]) -> None:
|
|
249
262
|
if _is_cmux_run(manifest):
|
|
250
|
-
# Under cmux okstra owns the panes for every lead, so this command is no
|
|
251
|
-
# longer the external lead's private door and the advice below no longer
|
|
252
|
-
# applies — there is nowhere else for a codex or Claude Code lead to go.
|
|
253
263
|
return
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if
|
|
257
|
-
|
|
258
|
-
raise DispatchError(
|
|
264
|
+
runtime = _require_string(manifest, "leadRuntime")
|
|
265
|
+
descriptor = default_host_registry().resolve(runtime).descriptor
|
|
266
|
+
if descriptor.launch_mode == "team":
|
|
267
|
+
return
|
|
268
|
+
raise DispatchError(
|
|
269
|
+
f"leadRuntime={descriptor.id} does not use the okstra team lifecycle"
|
|
270
|
+
)
|
|
259
271
|
|
|
260
272
|
|
|
261
273
|
def _load_manifest(project_root: str, run_manifest: str) -> dict[str, Any]:
|