okstra 0.175.1 → 0.176.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/docs/architecture/storage-model.md +2 -2
- package/docs/architecture.md +22 -19
- package/docs/cli.md +18 -14
- package/docs/for-ai/skills/okstra-inspect.md +2 -3
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/project-structure-overview.md +57 -56
- package/docs/task-process/README.md +11 -9
- package/docs/task-process/common-flow.md +13 -16
- package/docs/task-process/error-analysis.md +9 -10
- package/docs/task-process/final-verification.md +7 -7
- package/docs/task-process/implementation-planning.md +9 -9
- package/docs/task-process/implementation.md +6 -6
- package/docs/task-process/release-handoff.md +8 -7
- package/docs/task-process/requirements-discovery.md +8 -8
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/interactive.sh +12 -6
- package/runtime/bin/lib/okstra/usage.sh +3 -2
- package/runtime/bin/okstra-spawn-followups.py +4 -2
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/lead/context-loader.md +1 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +3 -4
- package/runtime/prompts/lead/report-writer.md +15 -1
- package/runtime/prompts/lead/team-contract.md +16 -12
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +0 -3
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
- package/runtime/prompts/profiles/change-impact-analysis.md +9 -5
- package/runtime/prompts/profiles/error-analysis.md +14 -8
- package/runtime/prompts/profiles/feature-analysis.md +9 -5
- package/runtime/prompts/profiles/final-verification.md +10 -7
- package/runtime/prompts/profiles/implementation-option-selection.md +9 -5
- package/runtime/prompts/profiles/implementation-planning.md +13 -7
- package/runtime/prompts/profiles/implementation.md +9 -5
- package/runtime/prompts/profiles/improvement-discovery.md +10 -6
- package/runtime/prompts/profiles/project-analysis.md +9 -5
- package/runtime/prompts/profiles/requirements-discovery.md +15 -9
- package/runtime/prompts/wizard/prompts.ko.json +18 -1
- package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
- package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
- package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +27 -27
- package/runtime/python/okstra_ctl/dispatch_core.py +120 -100
- package/runtime/python/okstra_ctl/domain/host.py +3 -1
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
- package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
- package/runtime/python/okstra_ctl/legacy_model_selection.py +115 -27
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/next_phase.py +236 -0
- package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
- package/runtime/python/okstra_ctl/recap.py +4 -1
- package/runtime/python/okstra_ctl/render.py +46 -47
- package/runtime/python/okstra_ctl/role_requirements.py +28 -35
- package/runtime/python/okstra_ctl/rollup.py +4 -1
- package/runtime/python/okstra_ctl/run.py +8 -5
- package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
- package/runtime/python/okstra_ctl/team.py +14 -18
- package/runtime/python/okstra_ctl/wizard.py +248 -56
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
- package/runtime/python/okstra_ctl/workflow.py +18 -32
- package/runtime/python/okstra_ctl/worktree.py +3 -3
- package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
- package/runtime/python/okstra_project/state.py +54 -6
- package/runtime/schemas/final-report-v2.0.schema.json +43 -5
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +15 -13
- package/runtime/skills/okstra-rollup/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +5 -5
- package/runtime/templates/implementation-worker-preamble.md +3 -18
- package/runtime/templates/project-docs/task-index.template.md +0 -1
- package/runtime/templates/reports/html/macros/forms.html +5 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
- package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +3 -36
- package/runtime/validators/validate-run.py +57 -99
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
roles:
|
|
5
5
|
- role: verifier
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
min: 2
|
|
7
|
+
recommended: 2
|
|
8
|
+
max: 5
|
|
8
9
|
duty: acceptance-verifier
|
|
9
|
-
minDistinctProviders: 2
|
|
10
10
|
- role: critic
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
min: 0
|
|
12
|
+
recommended: 0
|
|
13
|
+
max: 1
|
|
13
14
|
duty: acceptance-critic
|
|
14
15
|
- role: report-writer
|
|
15
|
-
|
|
16
|
+
min: 1
|
|
17
|
+
recommended: 1
|
|
18
|
+
max: 1
|
|
16
19
|
duty: report-writer
|
|
17
20
|
```
|
|
18
21
|
|
|
@@ -79,7 +82,7 @@ roles:
|
|
|
79
82
|
- **Validation Evidence**: for every requirement in the originating plan or task brief, cite the artifact (commit SHA, test output, log line, MCP SELECT result) that demonstrates coverage. Paraphrased "verified" claims without an artifact are rejected.
|
|
80
83
|
- **Read-only command log**: any pre-existing test/validation command touched during this run MUST be listed with its exact command line and one honest status — `executed` (ran; carries its exit code) / `advisory` (external Tier 3 did not PASS; carries observed/expected results and remains user-owned) / `env-unavailable` (should run but cannot in this environment — missing replica DB, container, or service; carries the reason, never a faked pass) / `not-configured` (no such qa-command tier) / `rejected` (a mutating/denied token — skipped, carries the denied token). A check that could not run locally is recorded as `env-unavailable` or `advisory` according to the external QA policy — never silently dropped and never reported as `executed` with an invented exit code. Mutating-command prohibition is the shared read-only boundary (see Non-goals); it is not restated per row.
|
|
81
84
|
- **Could-not-verify roll-up (§5.8.9)**: the template mechanically aggregates every not-confirmed check into one scannable list — `gap` requirement-coverage rows, `advisory` / `not-configured` / `env-unavailable` / `rejected` command rows, and `blocked` manual tests. You do not hand-author it, but you MUST give those rows their honest status so nothing unverified hides across sections: a check silently recorded as `executed`/`covered` will not surface in the roll-up. This is okstra's answer to "say what could not be verified this run."
|
|
82
|
-
- **Routing recommendation**:
|
|
85
|
+
- **Routing recommendation**: `finalVerification.routingRecommendation` is an **object** with exactly two fields — `target`, one value of the enum below, and `rationale`, the sentence tying that choice to the verdict and the blocker list. Free routing prose is not the field; a target named only in the prose does not route the task, because Phase 7 projects `workflow.nextRecommendedPhase` from `target` alone. The seven allowed targets are `release-handoff`, `release-handoff(stage-group)`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, and `done`. Both `release-handoff` forms are allowed ONLY when the Verdict Token is `accepted`. Plain `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` accepted run routes to `release-handoff(stage-group)` (or `implementation` / `done`) instead. `done` ends the lifecycle here. Enforcement: `schemas/final-report-v2.0.schema.json` rejects a `target` outside the enum, a missing `rationale`, and a string in place of the object; `validators/validate-run.py` rejects a missing `target`, a non-`accepted` verdict routed to either `release-handoff` form, and a `single-stage` report whose routing cites plain `release-handoff`.
|
|
83
86
|
- **Verified-row recording** (single-stage scope only): when the Verdict Token is `accepted`, the lead MUST run `okstra handoff record-verified --plan-run-root <plan-run-root> --stage <N> --report-path <final-report.md path> --data-json <final-report data.json path>` and quote the command + exit code in the report. The helper re-validates taskType/scope/verdict from data.json, so a non-accepted or whole-task report is rejected at the tool layer. **Enforced:** `validators/validate-run.py` `_validate_verified_row_recorded` requires a `verified` row in `runs/implementation-planning/consumers.jsonl` for every accepted stage — the helper validated its own inputs but nothing checked it had ever run, leaving reports that said `accepted` while the registry said unverified, so the stage was never offered for a stage-group PR.
|
|
84
87
|
- Clarification request policy (phase-specific addendum — shared policy is in `_common-contract.md`):
|
|
85
88
|
- populate `## 1. Clarification Items` only when a blocker hinges on information only the user can supply (deployment intent, intended target environment, business-rule interpretation); use `Blocks=next-phase` for items that gate continuing to release-handoff
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
roles:
|
|
5
5
|
- role: designer
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
min: 3
|
|
7
|
+
recommended: 3
|
|
8
|
+
max: 5
|
|
8
9
|
duty: direction-selection-worker
|
|
9
|
-
minDistinctProviders: 3
|
|
10
10
|
- role: report-writer
|
|
11
|
-
|
|
11
|
+
min: 1
|
|
12
|
+
recommended: 1
|
|
13
|
+
max: 1
|
|
12
14
|
duty: report-writer
|
|
13
15
|
- role: verifier
|
|
14
|
-
|
|
16
|
+
min: 0
|
|
17
|
+
recommended: 0
|
|
18
|
+
max: 0
|
|
15
19
|
duty: reverification-worker
|
|
16
20
|
dynamic: true
|
|
17
21
|
```
|
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
roles:
|
|
5
5
|
- role: planner
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
min: 2
|
|
7
|
+
recommended: 2
|
|
8
|
+
max: 5
|
|
8
9
|
duty: planning-worker
|
|
9
|
-
minDistinctProviders: 2
|
|
10
10
|
- role: critic
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
min: 0
|
|
12
|
+
recommended: 0
|
|
13
|
+
max: 1
|
|
13
14
|
duty: acceptance-critic
|
|
14
15
|
- role: report-writer
|
|
15
|
-
|
|
16
|
+
min: 1
|
|
17
|
+
recommended: 1
|
|
18
|
+
max: 1
|
|
16
19
|
duty: report-writer
|
|
17
20
|
- role: verifier
|
|
18
|
-
|
|
21
|
+
min: 0
|
|
22
|
+
recommended: 0
|
|
23
|
+
max: 0
|
|
19
24
|
duty: reverification-worker
|
|
20
25
|
dynamic: true
|
|
21
26
|
```
|
|
@@ -36,6 +41,7 @@ roles:
|
|
|
36
41
|
- never plan around an unconfirmed `intent-inference` augmentation as if it were a settled requirement. Treat the inference as settled ONLY when a `[CONFIRMED …]` marker sits on the matching `intent-check:` row after the precondition runs; absent the marker it stays a `Blocks=approval` clarification item per the precondition's `skipped` branch.
|
|
37
42
|
- `conversion-block:` rows are handled by the precondition; planning around an untranslated reporter phrase is forbidden until it is resolved.
|
|
38
43
|
- Worker planning procedure:
|
|
44
|
+
- **Ticket Tagging.** Tag every section 1–5 item with its related ticket. Use `Issue / Ticket`, fall back to Task ID, then `unknown`; comma-separate multiple tickets.
|
|
39
45
|
- route by the planning input contract before analysis. A run carrying `selected-direction.json` uses the selected-direction procedure. Only a legacy rerun without that snapshot uses candidate comparison.
|
|
40
46
|
- **Selected-direction planning procedure** — perform these steps in order and no others:
|
|
41
47
|
1. Read `selected-direction.json` and the original requirements ledger end-to-end.
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
roles:
|
|
5
5
|
- role: implementer
|
|
6
|
-
|
|
6
|
+
min: 1
|
|
7
|
+
recommended: 1
|
|
8
|
+
max: 1
|
|
7
9
|
duty: implementation-executor
|
|
8
10
|
- role: verifier
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
min: 2
|
|
12
|
+
recommended: 2
|
|
13
|
+
max: 3
|
|
11
14
|
duty: implementation-verifier
|
|
12
|
-
minDistinctProviders: 2
|
|
13
15
|
- role: report-writer
|
|
14
|
-
|
|
16
|
+
min: 1
|
|
17
|
+
recommended: 1
|
|
18
|
+
max: 1
|
|
15
19
|
duty: report-writer
|
|
16
20
|
```
|
|
17
21
|
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
roles:
|
|
5
5
|
- role: analyser
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
min: 3
|
|
7
|
+
recommended: 3
|
|
8
|
+
max: 5
|
|
8
9
|
duty: discovery-worker
|
|
9
|
-
minDistinctProviders: 3
|
|
10
10
|
- role: report-writer
|
|
11
|
-
|
|
11
|
+
min: 1
|
|
12
|
+
recommended: 1
|
|
13
|
+
max: 1
|
|
12
14
|
duty: report-writer
|
|
13
15
|
- role: verifier
|
|
14
|
-
|
|
16
|
+
min: 0
|
|
17
|
+
recommended: 0
|
|
18
|
+
max: 0
|
|
15
19
|
duty: reverification-worker
|
|
16
20
|
dynamic: true
|
|
17
21
|
```
|
|
@@ -57,7 +61,7 @@ roles:
|
|
|
57
61
|
- Stop conditions (OR): all questions resolved / budget exhausted / user signals proceed.
|
|
58
62
|
- Lead persists the round at `<RUN_DIR>/state/phase-1.5-grilling.md` with one section per question (question / recommended / user answer) and a closing `Resolved scope` / `Resolved lenses` block. Worker prompts use this resolved block as the authoritative scope and lens definition.
|
|
59
63
|
- The same log includes `## Primary Pass Assignments` with a `Worker ID | Primary lens` table. It contains every selected analyser exactly once in `requiredWorkerRoles` order; the lead derives it from the resolved roster and lenses rather than provider catalog order.
|
|
60
|
-
- After writing the log and before Phase 4 dispatch, the lead injects its **absolute path** into every analyser prompt as the `**Phase 1.5 Grilling Log:** <absolute-path>` anchor header (see `
|
|
64
|
+
- After writing the log and before Phase 4 dispatch, the lead injects its **absolute path** into every analyser prompt as the `**Phase 1.5 Grilling Log:** <absolute-path>` anchor header (see `okstra_ctl.worker_prompt_headers.worker_prompt_headers()`). This is the improvement-discovery counterpart to the `**Worktree:**` / `**Verification …:**` anchors that implementation / final-verification inject: workers read the log from this explicit path rather than re-deriving `<RUN_DIR>`. The path is byte-identical across all analysers, so it does not break the dispatch-prompt invariant.
|
|
61
65
|
- Decision-tree walk (bounded):
|
|
62
66
|
- When candidates branch on a structural question (e.g. "is module X meant to own this responsibility?"), resolve via `Read` / `Grep` first. Only escalate to the user inside the Phase 1.5 budget.
|
|
63
67
|
- Expected output emphasis:
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
roles:
|
|
5
5
|
- role: analyser
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
min: 2
|
|
7
|
+
recommended: 2
|
|
8
|
+
max: 5
|
|
8
9
|
duty: analysis-worker
|
|
9
|
-
minDistinctProviders: 2
|
|
10
10
|
- role: report-writer
|
|
11
|
-
|
|
11
|
+
min: 1
|
|
12
|
+
recommended: 1
|
|
13
|
+
max: 1
|
|
12
14
|
duty: report-writer
|
|
13
15
|
- role: verifier
|
|
14
|
-
|
|
16
|
+
min: 0
|
|
17
|
+
recommended: 0
|
|
18
|
+
max: 0
|
|
15
19
|
duty: reverification-worker
|
|
16
20
|
dynamic: true
|
|
17
21
|
```
|
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
```yaml
|
|
4
4
|
roles:
|
|
5
5
|
- role: analyser
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
min: 2
|
|
7
|
+
recommended: 2
|
|
8
|
+
max: 5
|
|
9
|
+
duty: discovery-worker
|
|
10
10
|
- role: critic
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
min: 0
|
|
12
|
+
recommended: 0
|
|
13
|
+
max: 1
|
|
13
14
|
duty: scope-critic
|
|
14
15
|
- role: report-writer
|
|
15
|
-
|
|
16
|
+
min: 1
|
|
17
|
+
recommended: 1
|
|
18
|
+
max: 1
|
|
16
19
|
duty: report-writer
|
|
17
20
|
- role: verifier
|
|
18
|
-
|
|
21
|
+
min: 0
|
|
22
|
+
recommended: 0
|
|
23
|
+
max: 0
|
|
19
24
|
duty: reverification-worker
|
|
20
25
|
dynamic: true
|
|
21
26
|
```
|
|
@@ -37,7 +42,8 @@ roles:
|
|
|
37
42
|
- `intent-inference` augmentations whose paired `intent-check:` row carries `[CONFIRMED …]` are treated as **confirmed**; trust the confirmation text in `## Reporter Confirmations` over the original inference if they differ. Unconfirmed `intent-inference` rows under `reporter-confirmations: skipped` follow the precondition's `skipped` branch above.
|
|
38
43
|
- `conversion-block:` rows are explicit "translation failed" signals — never attempt to resolve them by inference here; the precondition above already handled them.
|
|
39
44
|
- Worker discovery procedure:
|
|
40
|
-
-
|
|
45
|
+
- **Ticket Tagging.** Tag every section 1–5 item with its related ticket. Use `Issue / Ticket`, fall back to Task ID, then `unknown`; comma-separate multiple tickets.
|
|
46
|
+
- classify the request and cite the evidence that supports a recommended work category and safest next phase. The lead's Phase Routing settles the next phase.
|
|
41
47
|
- identify independently startable decomposition candidates without publishing or rendering fan-out artifacts; preserve every directed dependency from `Related Task Graph`
|
|
42
48
|
- resolve codebase-answerable ambiguity by inspection and record file:line evidence; return only human-owned decisions as clarification candidates with the evidence already checked
|
|
43
49
|
- state the reporter's rejection criteria, missing routing inputs, and the evidence boundary behind each recommendation
|
|
@@ -480,11 +480,28 @@
|
|
|
480
480
|
"no": "아니오 — 단계별로 다시 입력"
|
|
481
481
|
}
|
|
482
482
|
},
|
|
483
|
+
"leader_session": {
|
|
484
|
+
"label": "리더는 현재 세션 모델을 씁니다 (읽기 전용): {model_ref}{effort_suffix}",
|
|
485
|
+
"echo_template": "leader-session: {value}",
|
|
486
|
+
"options": {
|
|
487
|
+
"continue": "계속"
|
|
488
|
+
},
|
|
489
|
+
"effort_suffix": " · effort {effort}"
|
|
490
|
+
},
|
|
483
491
|
"role_count": {
|
|
484
|
-
"label": "{role} 역할 인스턴스 수를 선택하세요 ({minimum}..{maximum},
|
|
492
|
+
"label": "{role} 역할 인스턴스 수를 선택하세요 ({minimum}..{maximum}, 적정 {default})",
|
|
485
493
|
"echo_template": "role-count: {value}",
|
|
486
494
|
"options": {
|
|
487
495
|
"count": "{count}개{default_suffix}",
|
|
496
|
+
"default_suffix": " (적정)"
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
"role_add": {
|
|
500
|
+
"label": "선택 역할 {role} 을(를) 이번 run 에 추가할까요? (최대 {maximum}개, 기본: 추가 안 함)",
|
|
501
|
+
"echo_template": "role-add: {value}",
|
|
502
|
+
"options": {
|
|
503
|
+
"skip": "추가 안 함{default_suffix}",
|
|
504
|
+
"add": "{count}개 추가",
|
|
488
505
|
"default_suffix": " (기본)"
|
|
489
506
|
}
|
|
490
507
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Execution-surface adapters."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Build the injected execution-surface chain for one planned surface."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
from ...domain.worker_runtime import SURFACE_CLI_WRAPPER, SURFACE_CMUX_PANE
|
|
7
|
+
from ...ports.worker_runtime import WorkerRuntimePort
|
|
8
|
+
from .cli_wrapper import CliWrapperRuntime
|
|
9
|
+
from .cmux import CmuxRuntime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def runtime_chain(planned_surface: str) -> tuple[WorkerRuntimePort, ...]:
|
|
13
|
+
cli = CliWrapperRuntime()
|
|
14
|
+
if planned_surface == SURFACE_CLI_WRAPPER:
|
|
15
|
+
return (cli,)
|
|
16
|
+
if planned_surface == SURFACE_CMUX_PANE:
|
|
17
|
+
return (CmuxRuntime(), cli)
|
|
18
|
+
raise ValueError(f"unknown execution surface: {planned_surface}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def port_for(
|
|
22
|
+
chain: Sequence[WorkerRuntimePort], surface: str
|
|
23
|
+
) -> WorkerRuntimePort:
|
|
24
|
+
for port in chain:
|
|
25
|
+
if port.surface == surface:
|
|
26
|
+
return port
|
|
27
|
+
raise KeyError(surface)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Run a worker as a lead-owned subprocess."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from ...domain.worker_runtime import (
|
|
8
|
+
SURFACE_CLI_WRAPPER,
|
|
9
|
+
ProgressEvent,
|
|
10
|
+
RuntimeHandle,
|
|
11
|
+
WorkerSpawnRequest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CliWrapperRuntime:
|
|
16
|
+
surface = SURFACE_CLI_WRAPPER
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._children: dict[str, subprocess.Popen[str]] = {}
|
|
20
|
+
|
|
21
|
+
def spawn(self, request: WorkerSpawnRequest) -> RuntimeHandle:
|
|
22
|
+
process = subprocess.Popen(
|
|
23
|
+
list(request.command), cwd=request.cwd, text=True
|
|
24
|
+
)
|
|
25
|
+
identifier = uuid.uuid4().hex
|
|
26
|
+
self._children[identifier] = process
|
|
27
|
+
return RuntimeHandle(
|
|
28
|
+
surface=self.surface,
|
|
29
|
+
identifier=identifier,
|
|
30
|
+
waitable=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def close(self, handle: RuntimeHandle) -> None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def wait(self, handle: RuntimeHandle) -> int:
|
|
37
|
+
process = self._children[handle.identifier]
|
|
38
|
+
return process.wait()
|
|
39
|
+
|
|
40
|
+
def terminate(self, handle: RuntimeHandle) -> None:
|
|
41
|
+
process = self._children[handle.identifier]
|
|
42
|
+
if process.poll() is None:
|
|
43
|
+
process.terminate()
|
|
44
|
+
|
|
45
|
+
def notify(self, event: ProgressEvent) -> None:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def restore_lead(self) -> None:
|
|
49
|
+
return None
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Open and signal workers on a cmux pane surface."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from ... import cmux
|
|
7
|
+
from ...domain.worker_runtime import (
|
|
8
|
+
SURFACE_CMUX_PANE,
|
|
9
|
+
EnvironmentBlocked,
|
|
10
|
+
ProgressEvent,
|
|
11
|
+
RuntimeHandle,
|
|
12
|
+
SurfaceUnavailable,
|
|
13
|
+
WorkerSpawnRequest,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CmuxRuntime:
|
|
18
|
+
surface = SURFACE_CMUX_PANE
|
|
19
|
+
|
|
20
|
+
def spawn(self, request: WorkerSpawnRequest) -> RuntimeHandle:
|
|
21
|
+
workspace = cmux.resolve_lead_workspace()
|
|
22
|
+
if not workspace:
|
|
23
|
+
reason = cmux.unreachable_reason()
|
|
24
|
+
if reason in (cmux.LOST_ENVIRONMENT, cmux.LOST_DENIED):
|
|
25
|
+
observed = (
|
|
26
|
+
"this process has no cmux environment left — a sandbox sanitized it"
|
|
27
|
+
if reason == cmux.LOST_ENVIRONMENT
|
|
28
|
+
else "cmux is running but this process may not connect to its socket"
|
|
29
|
+
)
|
|
30
|
+
raise EnvironmentBlocked(
|
|
31
|
+
"this run was prepared for cmux panes, but "
|
|
32
|
+
f"{observed}. The same sandbox blocks worker CLIs from "
|
|
33
|
+
"their own config, so falling back to blocking workers "
|
|
34
|
+
"would fail too. Relaunch the lead outside a sandbox "
|
|
35
|
+
"(use a full-access lead sandbox)."
|
|
36
|
+
)
|
|
37
|
+
raise SurfaceUnavailable("cmux lead workspace is gone")
|
|
38
|
+
try:
|
|
39
|
+
surface_id = cmux.spawn_worker_surface(
|
|
40
|
+
workspace=workspace,
|
|
41
|
+
cwd=request.cwd,
|
|
42
|
+
command=request.command,
|
|
43
|
+
title=request.title,
|
|
44
|
+
owned_surface_ids=request.owned_surface_ids,
|
|
45
|
+
)
|
|
46
|
+
except (RuntimeError, OSError, subprocess.SubprocessError) as exc:
|
|
47
|
+
raise SurfaceUnavailable(str(exc)) from exc
|
|
48
|
+
return RuntimeHandle(
|
|
49
|
+
surface=self.surface,
|
|
50
|
+
identifier=surface_id,
|
|
51
|
+
waitable=False,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def close(self, handle: RuntimeHandle) -> None:
|
|
55
|
+
cmux.close_surface(handle.identifier)
|
|
56
|
+
|
|
57
|
+
def wait(self, handle: RuntimeHandle) -> int:
|
|
58
|
+
raise RuntimeError("cmux-pane handles are not waitable")
|
|
59
|
+
|
|
60
|
+
def terminate(self, handle: RuntimeHandle) -> None:
|
|
61
|
+
self.close(handle)
|
|
62
|
+
|
|
63
|
+
def notify(self, event: ProgressEvent) -> None:
|
|
64
|
+
workspace = cmux.resolve_lead_workspace()
|
|
65
|
+
cmux.sidebar_log(workspace, event.message, level=event.level)
|
|
66
|
+
if event.notify_title is not None:
|
|
67
|
+
cmux.sidebar_notify(
|
|
68
|
+
workspace,
|
|
69
|
+
title=event.notify_title,
|
|
70
|
+
body=event.notify_body or "",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def restore_lead(self) -> None:
|
|
74
|
+
cmux.restore_lead_width()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Choose an execution surface from an injected port chain."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
|
|
6
|
+
from ..domain.worker_runtime import (
|
|
7
|
+
EnvironmentBlocked,
|
|
8
|
+
RuntimeHandle,
|
|
9
|
+
SurfaceUnavailable,
|
|
10
|
+
WorkerSpawnRequest,
|
|
11
|
+
)
|
|
12
|
+
from ..ports.worker_runtime import WorkerRuntimePort
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def open_worker(
|
|
16
|
+
chain: Sequence[WorkerRuntimePort],
|
|
17
|
+
request_for: Callable[[str], WorkerSpawnRequest],
|
|
18
|
+
) -> RuntimeHandle:
|
|
19
|
+
last_unavailable: SurfaceUnavailable | None = None
|
|
20
|
+
for port in chain:
|
|
21
|
+
try:
|
|
22
|
+
return port.spawn(request_for(port.surface))
|
|
23
|
+
except EnvironmentBlocked:
|
|
24
|
+
raise
|
|
25
|
+
except SurfaceUnavailable as exc:
|
|
26
|
+
last_unavailable = exc
|
|
27
|
+
if last_unavailable is None:
|
|
28
|
+
raise SurfaceUnavailable("runtime chain is empty")
|
|
29
|
+
raise last_unavailable
|
|
@@ -339,7 +339,7 @@ def resolve_model_selection(
|
|
|
339
339
|
|
|
340
340
|
|
|
341
341
|
def _static_requirements(profile: RoleProfile) -> tuple[RoleRequirement, ...]:
|
|
342
|
-
leader = RoleRequirement("leader", 1, "lead")
|
|
342
|
+
leader = RoleRequirement("leader", 1, 1, 1, "lead")
|
|
343
343
|
return (leader, *(row for row in profile.roles if not row.dynamic))
|
|
344
344
|
|
|
345
345
|
|
|
@@ -355,17 +355,17 @@ def _role_instances(
|
|
|
355
355
|
)
|
|
356
356
|
instances: list[RoleInstance] = []
|
|
357
357
|
for requirement in requirements:
|
|
358
|
-
|
|
358
|
+
fixed = requirement.min_count == requirement.max_count
|
|
359
|
+
if requirement.role in role_counts and fixed:
|
|
359
360
|
raise AssignmentResolutionError(
|
|
360
361
|
f"role {requirement.role!r} has a fixed quantity and cannot "
|
|
361
362
|
"receive an explicit count"
|
|
362
363
|
)
|
|
363
|
-
count = role_counts.get(requirement.role, requirement.
|
|
364
|
-
|
|
365
|
-
if count < requirement.count or count > maximum:
|
|
364
|
+
count = role_counts.get(requirement.role, requirement.recommended_count)
|
|
365
|
+
if count < requirement.min_count or count > requirement.max_count:
|
|
366
366
|
raise AssignmentResolutionError(
|
|
367
367
|
f"role {requirement.role!r} count must be in "
|
|
368
|
-
f"{requirement.
|
|
368
|
+
f"{requirement.min_count}..{requirement.max_count}: {count}"
|
|
369
369
|
)
|
|
370
370
|
instances.extend(
|
|
371
371
|
RoleInstance(requirement.role, requirement.duty, ordinal)
|
|
@@ -747,20 +747,16 @@ def _constraint_failures(
|
|
|
747
747
|
) -> tuple[UnsatisfiedConstraint, ...]:
|
|
748
748
|
failures: list[UnsatisfiedConstraint] = []
|
|
749
749
|
for requirement in requirements:
|
|
750
|
-
|
|
751
|
-
if
|
|
750
|
+
role_rows = [row for row in assignments if row.role == requirement.role]
|
|
751
|
+
if not role_rows:
|
|
752
752
|
continue
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
for row in assignments
|
|
756
|
-
if row.role == requirement.role and row.ordinal <= requirement.count
|
|
757
|
-
}
|
|
758
|
-
if len(providers) < required:
|
|
753
|
+
refs = {row.model_ref for row in role_rows}
|
|
754
|
+
if len(refs) < len(role_rows):
|
|
759
755
|
failures.append(UnsatisfiedConstraint(
|
|
760
756
|
requirement.role,
|
|
761
|
-
"
|
|
762
|
-
|
|
763
|
-
len(
|
|
757
|
+
"uniqueModelRefs",
|
|
758
|
+
len(role_rows),
|
|
759
|
+
len(refs),
|
|
764
760
|
))
|
|
765
761
|
return tuple(failures)
|
|
766
762
|
|
|
@@ -771,20 +767,24 @@ def _unmet_constraints(
|
|
|
771
767
|
) -> tuple[UnsatisfiedConstraint, ...]:
|
|
772
768
|
failures: list[UnsatisfiedConstraint] = []
|
|
773
769
|
for requirement in requirements:
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
continue
|
|
777
|
-
providers = {
|
|
778
|
-
row.provider_id
|
|
770
|
+
role_groups = [
|
|
771
|
+
candidates
|
|
779
772
|
for candidates in options
|
|
773
|
+
if candidates and candidates[0].role == requirement.role
|
|
774
|
+
]
|
|
775
|
+
if not role_groups:
|
|
776
|
+
continue
|
|
777
|
+
worker_count = len(role_groups)
|
|
778
|
+
unique_refs = {
|
|
779
|
+
row.model_ref
|
|
780
|
+
for candidates in role_groups
|
|
780
781
|
for row in candidates
|
|
781
|
-
if row.role == requirement.role and row.ordinal <= requirement.count
|
|
782
782
|
}
|
|
783
|
-
if len(
|
|
783
|
+
if len(unique_refs) < worker_count:
|
|
784
784
|
failures.append(UnsatisfiedConstraint(
|
|
785
785
|
requirement.role,
|
|
786
|
-
"
|
|
787
|
-
|
|
788
|
-
len(
|
|
786
|
+
"uniqueModelRefs",
|
|
787
|
+
worker_count,
|
|
788
|
+
len(unique_refs),
|
|
789
789
|
))
|
|
790
790
|
return tuple(failures)
|