okstra 0.128.0 → 0.129.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 +15 -2
- package/docs/architecture.md +19 -3
- package/docs/cli.md +8 -0
- package/docs/project-structure-overview.md +14 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +3 -3
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +5 -5
- package/runtime/prompts/lead/adapters/claude-code.md +2 -1
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/convergence.md +42 -84
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -8
- package/runtime/prompts/lead/report-writer.md +3 -2
- package/runtime/prompts/lead/team-contract.md +11 -9
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -0
- package/runtime/prompts/profiles/improvement-discovery.md +11 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +7 -13
- package/runtime/python/okstra_ctl/codex_dispatch.py +42 -97
- package/runtime/python/okstra_ctl/context_cost.py +82 -7
- package/runtime/python/okstra_ctl/convergence.py +223 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
- package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
- package/runtime/python/okstra_ctl/convergence_store.py +85 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -14
- package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
- package/runtime/python/okstra_ctl/path_hints.py +23 -1
- package/runtime/python/okstra_ctl/paths.py +10 -1
- package/runtime/python/okstra_ctl/render.py +56 -11
- package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +102 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +79 -9
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
- package/runtime/templates/implementation-worker-preamble.md +51 -0
- package/runtime/templates/operating-standard.md +4 -4
- package/runtime/templates/report-writer-prompt-preamble.md +37 -0
- package/runtime/templates/worker-error-contract.md +46 -0
- package/runtime/templates/worker-prompt-preamble.md +28 -234
- package/runtime/validators/lib/fixtures.sh +27 -12
- package/runtime/validators/validate-run.py +201 -72
- package/src/cli-registry.mjs +7 -0
- package/src/commands/execute/convergence.mjs +34 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Resume and legacy-state migration decisions for convergence seeding."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import tempfile
|
|
9
|
+
from typing import Any, Literal, Mapping
|
|
10
|
+
|
|
11
|
+
from .convergence_engine import (
|
|
12
|
+
ConvergenceContractError,
|
|
13
|
+
grouped_input_digest,
|
|
14
|
+
validate_final_state,
|
|
15
|
+
validate_working_state,
|
|
16
|
+
)
|
|
17
|
+
from .convergence_store import load_json_object
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SeedAction = Literal[
|
|
21
|
+
"create-work",
|
|
22
|
+
"resume-work",
|
|
23
|
+
"reuse-final",
|
|
24
|
+
"restart-round0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class SeedDecision:
|
|
30
|
+
action: SeedAction
|
|
31
|
+
archive_path: Path | None
|
|
32
|
+
legacy_digest: str | None
|
|
33
|
+
reason: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decide_seed_action(
|
|
37
|
+
*,
|
|
38
|
+
grouped_input: Mapping[str, Any],
|
|
39
|
+
work_state_path: Path,
|
|
40
|
+
final_state_path: Path,
|
|
41
|
+
restart_from_round0: bool,
|
|
42
|
+
) -> SeedDecision:
|
|
43
|
+
"""Classify existing new-engine/legacy state without mutating files."""
|
|
44
|
+
task_key = grouped_input.get("taskKey")
|
|
45
|
+
digest = grouped_input_digest(grouped_input)
|
|
46
|
+
final_status, final_value = _inspect_final(final_state_path)
|
|
47
|
+
if final_status == "terminal":
|
|
48
|
+
return SeedDecision("reuse-final", None, None, "valid terminal final state")
|
|
49
|
+
|
|
50
|
+
work_status, work_value = _inspect_work(work_state_path)
|
|
51
|
+
invalid_final = final_status == "invalid"
|
|
52
|
+
if work_status == "valid":
|
|
53
|
+
assert work_value is not None
|
|
54
|
+
if work_value.get("taskKey") != task_key:
|
|
55
|
+
raise ConvergenceContractError(
|
|
56
|
+
"existing working state taskKey does not match grouped input"
|
|
57
|
+
)
|
|
58
|
+
if work_value.get("groupsDigest") != digest:
|
|
59
|
+
raise ConvergenceContractError(
|
|
60
|
+
"existing working state groupsDigest does not match grouped input"
|
|
61
|
+
)
|
|
62
|
+
if invalid_final:
|
|
63
|
+
archive_path, legacy_digest = _archive_identity(
|
|
64
|
+
final_state_path, final_state_path.parent / "migrations"
|
|
65
|
+
)
|
|
66
|
+
return SeedDecision(
|
|
67
|
+
"resume-work",
|
|
68
|
+
archive_path,
|
|
69
|
+
legacy_digest,
|
|
70
|
+
"matching working state with invalid legacy final",
|
|
71
|
+
)
|
|
72
|
+
return SeedDecision("resume-work", None, None, "matching working state")
|
|
73
|
+
|
|
74
|
+
if work_status == "invalid" and not restart_from_round0:
|
|
75
|
+
raise ConvergenceContractError(
|
|
76
|
+
"existing convergence working state is invalid; pass "
|
|
77
|
+
"--restart-from-round0 to archive it and restart"
|
|
78
|
+
)
|
|
79
|
+
if work_status == "invalid":
|
|
80
|
+
source = final_state_path if invalid_final else work_state_path
|
|
81
|
+
archive_path, legacy_digest = _archive_identity(
|
|
82
|
+
source, source.parent / "migrations"
|
|
83
|
+
)
|
|
84
|
+
return SeedDecision(
|
|
85
|
+
"restart-round0",
|
|
86
|
+
archive_path,
|
|
87
|
+
legacy_digest,
|
|
88
|
+
"explicit restart of invalid working state",
|
|
89
|
+
)
|
|
90
|
+
if invalid_final:
|
|
91
|
+
archive_path, legacy_digest = _archive_identity(
|
|
92
|
+
final_state_path, final_state_path.parent / "migrations"
|
|
93
|
+
)
|
|
94
|
+
return SeedDecision(
|
|
95
|
+
"restart-round0",
|
|
96
|
+
archive_path,
|
|
97
|
+
legacy_digest,
|
|
98
|
+
"invalid or partial legacy final state",
|
|
99
|
+
)
|
|
100
|
+
return SeedDecision("create-work", None, None, "no existing convergence state")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def archive_state_bytes(
|
|
104
|
+
*,
|
|
105
|
+
source_path: Path,
|
|
106
|
+
migration_dir: Path,
|
|
107
|
+
) -> tuple[Path, str]:
|
|
108
|
+
"""Archive source bytes under a digest-derived immutable filename."""
|
|
109
|
+
data = source_path.read_bytes()
|
|
110
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
111
|
+
archive_path = migration_dir / f"{source_path.name}.{digest[:12]}.json"
|
|
112
|
+
if archive_path.exists():
|
|
113
|
+
if archive_path.read_bytes() != data:
|
|
114
|
+
raise ConvergenceContractError(
|
|
115
|
+
f"migration archive path contains different bytes: {archive_path}"
|
|
116
|
+
)
|
|
117
|
+
return archive_path, digest
|
|
118
|
+
migration_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
temp_path: Path | None = None
|
|
120
|
+
try:
|
|
121
|
+
with tempfile.NamedTemporaryFile(
|
|
122
|
+
mode="wb",
|
|
123
|
+
dir=migration_dir,
|
|
124
|
+
prefix=f".{archive_path.name}.",
|
|
125
|
+
suffix=".tmp",
|
|
126
|
+
delete=False,
|
|
127
|
+
) as handle:
|
|
128
|
+
temp_path = Path(handle.name)
|
|
129
|
+
handle.write(data)
|
|
130
|
+
handle.flush()
|
|
131
|
+
os.fsync(handle.fileno())
|
|
132
|
+
os.replace(temp_path, archive_path)
|
|
133
|
+
temp_path = None
|
|
134
|
+
finally:
|
|
135
|
+
if temp_path is not None:
|
|
136
|
+
temp_path.unlink(missing_ok=True)
|
|
137
|
+
return archive_path, digest
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def migration_record(
|
|
141
|
+
source_path: Path,
|
|
142
|
+
archive_path: Path,
|
|
143
|
+
digest: str,
|
|
144
|
+
) -> dict[str, str]:
|
|
145
|
+
return {
|
|
146
|
+
"mode": "restart-round0",
|
|
147
|
+
"sourcePath": str(source_path),
|
|
148
|
+
"archivePath": str(archive_path),
|
|
149
|
+
"legacyDigest": digest,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _inspect_final(path: Path) -> tuple[str, dict[str, Any] | None]:
|
|
154
|
+
if not path.exists():
|
|
155
|
+
return "absent", None
|
|
156
|
+
try:
|
|
157
|
+
value = load_json_object(path)
|
|
158
|
+
except (OSError, ValueError):
|
|
159
|
+
return "invalid", None
|
|
160
|
+
if validate_final_state(value):
|
|
161
|
+
return "invalid", value
|
|
162
|
+
if value.get("finalState") not in {
|
|
163
|
+
"converged",
|
|
164
|
+
"max-rounds-reached",
|
|
165
|
+
"aborted-non-result",
|
|
166
|
+
}:
|
|
167
|
+
return "invalid", value
|
|
168
|
+
return "terminal", value
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _inspect_work(path: Path) -> tuple[str, dict[str, Any] | None]:
|
|
172
|
+
if not path.exists():
|
|
173
|
+
return "absent", None
|
|
174
|
+
try:
|
|
175
|
+
value = load_json_object(path)
|
|
176
|
+
except (OSError, ValueError):
|
|
177
|
+
return "invalid", None
|
|
178
|
+
return ("valid", value) if not validate_working_state(value) else ("invalid", value)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _archive_identity(source_path: Path, migration_dir: Path) -> tuple[Path, str]:
|
|
182
|
+
data = source_path.read_bytes()
|
|
183
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
184
|
+
return migration_dir / f"{source_path.name}.{digest[:12]}.json", digest
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Validated JSON loading and atomic persistence for convergence artifacts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import tempfile
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
|
|
11
|
+
from .convergence_engine import ConvergenceContractError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_json_object(path: Path) -> dict[str, Any]:
|
|
15
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
16
|
+
if not isinstance(value, dict):
|
|
17
|
+
raise ConvergenceContractError(f"JSON input must be an object: {path}")
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def write_json_atomic(path: Path, payload: Mapping[str, Any]) -> None:
|
|
22
|
+
if not isinstance(payload, Mapping):
|
|
23
|
+
raise ConvergenceContractError("JSON output payload must be an object")
|
|
24
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
temp_path: Path | None = None
|
|
26
|
+
try:
|
|
27
|
+
with tempfile.NamedTemporaryFile(
|
|
28
|
+
mode="w",
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
dir=path.parent,
|
|
31
|
+
prefix=f".{path.name}.",
|
|
32
|
+
suffix=".tmp",
|
|
33
|
+
delete=False,
|
|
34
|
+
) as handle:
|
|
35
|
+
temp_path = Path(handle.name)
|
|
36
|
+
json.dump(
|
|
37
|
+
payload,
|
|
38
|
+
handle,
|
|
39
|
+
ensure_ascii=False,
|
|
40
|
+
sort_keys=True,
|
|
41
|
+
indent=2,
|
|
42
|
+
)
|
|
43
|
+
handle.write("\n")
|
|
44
|
+
handle.flush()
|
|
45
|
+
os.fsync(handle.fileno())
|
|
46
|
+
os.replace(temp_path, path)
|
|
47
|
+
temp_path = None
|
|
48
|
+
finally:
|
|
49
|
+
if temp_path is not None:
|
|
50
|
+
temp_path.unlink(missing_ok=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def write_final_state_atomic(
|
|
54
|
+
path: Path,
|
|
55
|
+
payload: Mapping[str, Any],
|
|
56
|
+
*,
|
|
57
|
+
migration: Mapping[str, Any] | None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Replace a legacy final only when its byte archive proves preservation."""
|
|
60
|
+
if path.exists():
|
|
61
|
+
_validate_final_replacement(path, migration)
|
|
62
|
+
write_json_atomic(path, payload)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _validate_final_replacement(
|
|
66
|
+
path: Path,
|
|
67
|
+
migration: Mapping[str, Any] | None,
|
|
68
|
+
) -> None:
|
|
69
|
+
if not isinstance(migration, Mapping) or migration.get("mode") != "restart-round0":
|
|
70
|
+
raise ConvergenceContractError(
|
|
71
|
+
"existing final state requires a restart-round0 migration archive"
|
|
72
|
+
)
|
|
73
|
+
if migration.get("sourcePath") != str(path):
|
|
74
|
+
raise ConvergenceContractError("migration archive sourcePath does not match final state")
|
|
75
|
+
archive_value = migration.get("archivePath")
|
|
76
|
+
digest = migration.get("legacyDigest")
|
|
77
|
+
if not isinstance(archive_value, str) or not isinstance(digest, str):
|
|
78
|
+
raise ConvergenceContractError("migration archive metadata is incomplete")
|
|
79
|
+
archive_path = Path(archive_value)
|
|
80
|
+
if not archive_path.is_file():
|
|
81
|
+
raise ConvergenceContractError(f"migration archive is missing: {archive_path}")
|
|
82
|
+
archived = archive_path.read_bytes()
|
|
83
|
+
current = path.read_bytes()
|
|
84
|
+
if hashlib.sha256(archived).hexdigest() != digest or archived != current:
|
|
85
|
+
raise ConvergenceContractError("migration archive bytes do not match legacy final")
|
|
@@ -18,7 +18,9 @@ from .final_report_paths import (
|
|
|
18
18
|
from .lead_events import LeadEvent, append_lead_event
|
|
19
19
|
from .path_hints import hydrate_active_run_context
|
|
20
20
|
from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
|
|
21
|
-
from .
|
|
21
|
+
from .worker_prompt_body import analysis_prompt_body
|
|
22
|
+
from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
|
|
23
|
+
from .worker_prompt_policy import resolve_prompt_plan_for_manifest
|
|
22
24
|
from .wrapper_status import read_wrapper_status, status_path_for_prompt
|
|
23
25
|
|
|
24
26
|
|
|
@@ -184,7 +186,7 @@ def build_dispatch_plan(
|
|
|
184
186
|
report_writer_model,
|
|
185
187
|
options,
|
|
186
188
|
)
|
|
187
|
-
|
|
189
|
+
_validate_initial_prompts(manifest, jobs)
|
|
188
190
|
return DispatchPlan(
|
|
189
191
|
project_root=project_root,
|
|
190
192
|
workspace_root=workspace_root.resolve(),
|
|
@@ -197,22 +199,18 @@ def build_dispatch_plan(
|
|
|
197
199
|
)
|
|
198
200
|
|
|
199
201
|
|
|
200
|
-
def
|
|
202
|
+
def _validate_initial_prompts(
|
|
201
203
|
manifest: Mapping[str, Any],
|
|
202
204
|
jobs: Sequence[WorkerJob],
|
|
203
205
|
) -> None:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
prompt_paths = {
|
|
207
|
-
job.worker_id: job.prompt_path
|
|
206
|
+
records = [
|
|
207
|
+
PromptRecord(job.worker_id, job.dispatch_kind, job.prompt_path)
|
|
208
208
|
for job in jobs
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
and "-reverify-r" not in job.prompt_path.name
|
|
212
|
-
}
|
|
213
|
-
errors = validate_final_verification_prompt_paths(prompt_paths)
|
|
209
|
+
]
|
|
210
|
+
errors = validate_initial_prompt_records(manifest=manifest, records=records)
|
|
214
211
|
if errors:
|
|
215
|
-
|
|
212
|
+
task_type = _require_string(manifest, "taskType")
|
|
213
|
+
raise DispatchError(f"{task_type} prompt contract: " + "; ".join(errors))
|
|
216
214
|
|
|
217
215
|
|
|
218
216
|
def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
|
|
@@ -334,6 +332,9 @@ def _job_from_roster_worker(
|
|
|
334
332
|
project_root=project_root,
|
|
335
333
|
manifest=manifest,
|
|
336
334
|
active_context=active_context,
|
|
335
|
+
dispatch_kind=options.dispatch_kind,
|
|
336
|
+
model=model,
|
|
337
|
+
role=_string_value(state.get("role")) or "worker",
|
|
337
338
|
)
|
|
338
339
|
return WorkerJob(
|
|
339
340
|
worker_id=worker_id,
|
|
@@ -798,6 +799,9 @@ def _materialize_prompt_if_missing(
|
|
|
798
799
|
project_root: Path,
|
|
799
800
|
manifest: Mapping[str, Any],
|
|
800
801
|
active_context: Mapping[str, Any],
|
|
802
|
+
dispatch_kind: str,
|
|
803
|
+
model: str,
|
|
804
|
+
role: str,
|
|
801
805
|
) -> None:
|
|
802
806
|
if prompt_path.is_file():
|
|
803
807
|
return
|
|
@@ -808,12 +812,47 @@ def _materialize_prompt_if_missing(
|
|
|
808
812
|
prompt_rel=prompt_rel,
|
|
809
813
|
result_rel=result_rel,
|
|
810
814
|
worker_id=worker_id,
|
|
815
|
+
dispatch_kind=dispatch_kind,
|
|
811
816
|
manifest=manifest,
|
|
812
817
|
active_context=active_context,
|
|
813
818
|
)
|
|
814
819
|
except WorkerPromptHeaderError as exc:
|
|
815
820
|
raise DispatchError(str(exc)) from exc
|
|
816
|
-
|
|
821
|
+
try:
|
|
822
|
+
plan = resolve_prompt_plan_for_manifest(
|
|
823
|
+
manifest=manifest,
|
|
824
|
+
worker_id=worker_id,
|
|
825
|
+
dispatch_kind=dispatch_kind,
|
|
826
|
+
)
|
|
827
|
+
except ValueError as exc:
|
|
828
|
+
raise DispatchError(str(exc)) from exc
|
|
829
|
+
lines = list(headers)
|
|
830
|
+
if plan.audience in {
|
|
831
|
+
"analysis",
|
|
832
|
+
"implementation-executor",
|
|
833
|
+
"implementation-verifier",
|
|
834
|
+
}:
|
|
835
|
+
if _string_value(manifest.get("taskType")) == "implementation":
|
|
836
|
+
worktree = _worktree_path(active_context)
|
|
837
|
+
if not worktree:
|
|
838
|
+
raise DispatchError(
|
|
839
|
+
"implementation prompt generation requires a worktree path"
|
|
840
|
+
)
|
|
841
|
+
lines.append(f"**Worktree:** {worktree}")
|
|
842
|
+
try:
|
|
843
|
+
lines.extend(
|
|
844
|
+
analysis_prompt_body(
|
|
845
|
+
manifest,
|
|
846
|
+
active_context,
|
|
847
|
+
worker_id,
|
|
848
|
+
model,
|
|
849
|
+
role,
|
|
850
|
+
plan,
|
|
851
|
+
)
|
|
852
|
+
)
|
|
853
|
+
except ValueError as exc:
|
|
854
|
+
raise DispatchError(str(exc)) from exc
|
|
855
|
+
prompt_path.write_text("\n".join([*lines, ""]), encoding="utf-8")
|
|
817
856
|
|
|
818
857
|
|
|
819
858
|
def _provider_for_worker(worker_id: str) -> str:
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Assign improvement-discovery primary passes from the resolved roster."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def assign_primary_lenses(
|
|
8
|
+
worker_ids: list[str],
|
|
9
|
+
priority_lenses: list[str],
|
|
10
|
+
) -> dict[str, str]:
|
|
11
|
+
"""Round-robin resolved worker instances over resolved priority lenses."""
|
|
12
|
+
if not worker_ids:
|
|
13
|
+
raise ValueError("worker IDs must not be empty")
|
|
14
|
+
if not priority_lenses:
|
|
15
|
+
raise ValueError("priority lenses must not be empty")
|
|
16
|
+
if len(set(worker_ids)) != len(worker_ids):
|
|
17
|
+
raise ValueError("worker IDs must be unique")
|
|
18
|
+
return {
|
|
19
|
+
worker_id: priority_lenses[index % len(priority_lenses)]
|
|
20
|
+
for index, worker_id in enumerate(worker_ids)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_primary_lens_assignments(
|
|
25
|
+
assignments: Mapping[str, str],
|
|
26
|
+
worker_ids: list[str],
|
|
27
|
+
priority_lenses: list[str],
|
|
28
|
+
) -> list[str]:
|
|
29
|
+
"""Require every selected analyser once at its roster-order lens."""
|
|
30
|
+
errors: list[str] = []
|
|
31
|
+
if not worker_ids:
|
|
32
|
+
return ["resolved analyser worker IDs must not be empty"]
|
|
33
|
+
if not priority_lenses:
|
|
34
|
+
return ["resolved priority lenses must not be empty"]
|
|
35
|
+
try:
|
|
36
|
+
expected = assign_primary_lenses(worker_ids, priority_lenses)
|
|
37
|
+
except ValueError as exc:
|
|
38
|
+
return [str(exc)]
|
|
39
|
+
if list(assignments) != worker_ids:
|
|
40
|
+
errors.append(
|
|
41
|
+
"assignment order must match resolved analyser worker order: "
|
|
42
|
+
+ ", ".join(worker_ids)
|
|
43
|
+
)
|
|
44
|
+
for worker_id in worker_ids:
|
|
45
|
+
if worker_id not in assignments:
|
|
46
|
+
errors.append(f"missing worker assignment: {worker_id}")
|
|
47
|
+
for worker_id, lens in assignments.items():
|
|
48
|
+
if worker_id not in expected:
|
|
49
|
+
errors.append(f"extra worker assignment: {worker_id}")
|
|
50
|
+
continue
|
|
51
|
+
if lens not in priority_lenses:
|
|
52
|
+
errors.append(
|
|
53
|
+
f"{worker_id}: primary lens {lens!r} is outside resolved priority lenses"
|
|
54
|
+
)
|
|
55
|
+
continue
|
|
56
|
+
if lens != expected[worker_id]:
|
|
57
|
+
errors.append(
|
|
58
|
+
f"{worker_id}: expected primary lens {expected[worker_id]!r}, "
|
|
59
|
+
f"got {lens!r}"
|
|
60
|
+
)
|
|
61
|
+
return errors
|
|
@@ -98,6 +98,17 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
98
98
|
"errorLogs": _hydrate_active_error_logs(ctx),
|
|
99
99
|
"runtimeResources": {
|
|
100
100
|
"codingPreflightDir": ctx.get("OKSTRA_CODING_PREFLIGHT_DIR", ""),
|
|
101
|
+
"workerPreamblePathByAudience": {
|
|
102
|
+
"analysis": ctx.get("ANALYSIS_WORKER_PREAMBLE_PATH", ""),
|
|
103
|
+
"implementation-executor": ctx.get(
|
|
104
|
+
"IMPLEMENTATION_WORKER_PREAMBLE_PATH", ""
|
|
105
|
+
),
|
|
106
|
+
"implementation-verifier": ctx.get(
|
|
107
|
+
"IMPLEMENTATION_WORKER_PREAMBLE_PATH", ""
|
|
108
|
+
),
|
|
109
|
+
"report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH", ""),
|
|
110
|
+
},
|
|
111
|
+
"workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH", ""),
|
|
101
112
|
},
|
|
102
113
|
"executorWorktree": dict(_mapping(payload.get("executorWorktree"))),
|
|
103
114
|
"verificationTarget": dict(_mapping(payload.get("verificationTarget"))),
|
|
@@ -456,7 +467,18 @@ def _runtime_fields(paths: Mapping[str, Any]) -> dict[str, str]:
|
|
|
456
467
|
runtime_home = Path(paths["runtime_home"])
|
|
457
468
|
lead = runtime_home / "prompts" / "lead"
|
|
458
469
|
return {
|
|
459
|
-
"
|
|
470
|
+
"ANALYSIS_WORKER_PREAMBLE_PATH": str(
|
|
471
|
+
runtime_home / "templates" / "worker-prompt-preamble.md"
|
|
472
|
+
),
|
|
473
|
+
"IMPLEMENTATION_WORKER_PREAMBLE_PATH": str(
|
|
474
|
+
runtime_home / "templates" / "implementation-worker-preamble.md"
|
|
475
|
+
),
|
|
476
|
+
"REPORT_WRITER_PREAMBLE_PATH": str(
|
|
477
|
+
runtime_home / "templates" / "report-writer-prompt-preamble.md"
|
|
478
|
+
),
|
|
479
|
+
"WORKER_ERROR_CONTRACT_PATH": str(
|
|
480
|
+
runtime_home / "templates" / "worker-error-contract.md"
|
|
481
|
+
),
|
|
460
482
|
"OKSTRA_LEAD_CONTRACT_PATH": str(lead / "okstra-lead-contract.md"),
|
|
461
483
|
"OKSTRA_CONTEXT_LOADER_PATH": str(lead / "context-loader.md"),
|
|
462
484
|
"OKSTRA_TEAM_CONTRACT_PATH": str(lead / "team-contract.md"),
|
|
@@ -265,9 +265,18 @@ def compute_run_paths(
|
|
|
265
265
|
"TASK_TYPE_SEGMENT": task_type_segment,
|
|
266
266
|
"OKSTRA_ROOT": str(okstra_root),
|
|
267
267
|
"OKSTRA_TASKS_ROOT": str(tasks_root),
|
|
268
|
-
"
|
|
268
|
+
"ANALYSIS_WORKER_PREAMBLE_PATH": str(
|
|
269
269
|
runtime_home / "templates" / "worker-prompt-preamble.md"
|
|
270
270
|
),
|
|
271
|
+
"IMPLEMENTATION_WORKER_PREAMBLE_PATH": str(
|
|
272
|
+
runtime_home / "templates" / "implementation-worker-preamble.md"
|
|
273
|
+
),
|
|
274
|
+
"REPORT_WRITER_PREAMBLE_PATH": str(
|
|
275
|
+
runtime_home / "templates" / "report-writer-prompt-preamble.md"
|
|
276
|
+
),
|
|
277
|
+
"WORKER_ERROR_CONTRACT_PATH": str(
|
|
278
|
+
runtime_home / "templates" / "worker-error-contract.md"
|
|
279
|
+
),
|
|
271
280
|
"OKSTRA_LEAD_CONTRACT_PATH": str(
|
|
272
281
|
runtime_home / "prompts" / "lead" / "okstra-lead-contract.md"
|
|
273
282
|
),
|
|
@@ -376,6 +376,22 @@ def _active_runtime_resources(ctx: dict) -> dict:
|
|
|
376
376
|
return {
|
|
377
377
|
"codingPreflightDir": ctx.get("OKSTRA_CODING_PREFLIGHT_DIR")
|
|
378
378
|
or str(runtime_home / "prompts" / "coding-preflight"),
|
|
379
|
+
"workerPreamblePathByAudience": {
|
|
380
|
+
"analysis": ctx.get("ANALYSIS_WORKER_PREAMBLE_PATH")
|
|
381
|
+
or str(runtime_home / "templates" / "worker-prompt-preamble.md"),
|
|
382
|
+
"implementation-executor": ctx.get(
|
|
383
|
+
"IMPLEMENTATION_WORKER_PREAMBLE_PATH"
|
|
384
|
+
)
|
|
385
|
+
or str(runtime_home / "templates" / "implementation-worker-preamble.md"),
|
|
386
|
+
"implementation-verifier": ctx.get(
|
|
387
|
+
"IMPLEMENTATION_WORKER_PREAMBLE_PATH"
|
|
388
|
+
)
|
|
389
|
+
or str(runtime_home / "templates" / "implementation-worker-preamble.md"),
|
|
390
|
+
"report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH")
|
|
391
|
+
or str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
|
|
392
|
+
},
|
|
393
|
+
"workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH")
|
|
394
|
+
or str(runtime_home / "templates" / "worker-error-contract.md"),
|
|
379
395
|
}
|
|
380
396
|
|
|
381
397
|
|
|
@@ -1783,6 +1799,19 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1783
1799
|
coding_preflight_dir = ctx.get("OKSTRA_CODING_PREFLIGHT_DIR") or str(
|
|
1784
1800
|
home_prompts / "coding-preflight"
|
|
1785
1801
|
)
|
|
1802
|
+
runtime_templates = okstra_home() / "templates"
|
|
1803
|
+
analysis_preamble = ctx.get("ANALYSIS_WORKER_PREAMBLE_PATH") or str(
|
|
1804
|
+
runtime_templates / "worker-prompt-preamble.md"
|
|
1805
|
+
)
|
|
1806
|
+
implementation_preamble = ctx.get("IMPLEMENTATION_WORKER_PREAMBLE_PATH") or str(
|
|
1807
|
+
runtime_templates / "implementation-worker-preamble.md"
|
|
1808
|
+
)
|
|
1809
|
+
report_writer_preamble = ctx.get("REPORT_WRITER_PREAMBLE_PATH") or str(
|
|
1810
|
+
runtime_templates / "report-writer-prompt-preamble.md"
|
|
1811
|
+
)
|
|
1812
|
+
worker_error_contract = ctx.get("WORKER_ERROR_CONTRACT_PATH") or str(
|
|
1813
|
+
runtime_templates / "worker-error-contract.md"
|
|
1814
|
+
)
|
|
1786
1815
|
okstra_runtime_resources_block = (
|
|
1787
1816
|
"## Okstra Runtime Resources\n"
|
|
1788
1817
|
"\n"
|
|
@@ -1795,7 +1824,11 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1795
1824
|
f"- Convergence contract: `{convergence_path}`\n"
|
|
1796
1825
|
f"- Plan-body verification contract (implementation-planning Phase 6 sub-step only): `{plan_body_verification_path}`\n"
|
|
1797
1826
|
f"- Report writer contract: `{report_writer_path}`\n"
|
|
1798
|
-
f"- Coding preflight pack: `{coding_preflight_dir}
|
|
1827
|
+
f"- Coding preflight pack: `{coding_preflight_dir}`\n"
|
|
1828
|
+
f"- Analysis worker preamble: `{analysis_preamble}`\n"
|
|
1829
|
+
f"- Implementation worker preamble: `{implementation_preamble}`\n"
|
|
1830
|
+
f"- Report writer preamble: `{report_writer_preamble}`\n"
|
|
1831
|
+
f"- Shared worker error contract: `{worker_error_contract}`"
|
|
1799
1832
|
)
|
|
1800
1833
|
|
|
1801
1834
|
def fmt_assignment(role: str, model: str, execution: str) -> str:
|
|
@@ -1915,10 +1948,6 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1915
1948
|
"the selected adapter's mapping. Follow the lifecycle core's lazy-read rules "
|
|
1916
1949
|
"for support resources; do not invoke hidden/internal skills."
|
|
1917
1950
|
)
|
|
1918
|
-
worker_preamble_path = ctx.get(
|
|
1919
|
-
"WORKER_PROMPT_PREAMBLE_PATH",
|
|
1920
|
-
str(okstra_home() / "templates" / "worker-prompt-preamble.md"),
|
|
1921
|
-
)
|
|
1922
1951
|
worker_dispatch_guidance = (
|
|
1923
1952
|
"## Worker Dispatch Contract\n"
|
|
1924
1953
|
"\n"
|
|
@@ -1932,10 +1961,14 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1932
1961
|
"- Use `await_workers` through the selected adapter and confirm terminal state "
|
|
1933
1962
|
"plus every required Result Path. File presence alone is never a signal to "
|
|
1934
1963
|
"skip; worker selection and skipped status come from `team-state`.\n"
|
|
1935
|
-
"-
|
|
1936
|
-
|
|
1937
|
-
"
|
|
1938
|
-
"
|
|
1964
|
+
"- Resolve the worker's PromptPlan audience and inject exactly one "
|
|
1965
|
+
"`**Worker Preamble Path:**` from the audience map: "
|
|
1966
|
+
f"analysis=`{analysis_preamble}`, implementation executor/verifier="
|
|
1967
|
+
f"`{implementation_preamble}`, report-writer=`{report_writer_preamble}`.\n"
|
|
1968
|
+
"- Every initial worker prompt also injects "
|
|
1969
|
+
f"`**Worker Error Contract Path:** {worker_error_contract}`. The selected "
|
|
1970
|
+
"preamble owns audience procedure; the shared error contract owns error "
|
|
1971
|
+
"schema and write rules. Do not re-inline either contract."
|
|
1939
1972
|
)
|
|
1940
1973
|
|
|
1941
1974
|
if lead_runtime == "external":
|
|
@@ -2006,12 +2039,24 @@ def apply_lead_prompt_defaults(ctx: dict) -> None:
|
|
|
2006
2039
|
ctx.setdefault("STAGE_INTEGRATION", "")
|
|
2007
2040
|
runtime_home = okstra_home()
|
|
2008
2041
|
ctx.setdefault(
|
|
2009
|
-
"
|
|
2042
|
+
"ANALYSIS_WORKER_PREAMBLE_PATH",
|
|
2010
2043
|
str(runtime_home / "templates" / "worker-prompt-preamble.md"),
|
|
2011
2044
|
)
|
|
2045
|
+
ctx.setdefault(
|
|
2046
|
+
"IMPLEMENTATION_WORKER_PREAMBLE_PATH",
|
|
2047
|
+
str(runtime_home / "templates" / "implementation-worker-preamble.md"),
|
|
2048
|
+
)
|
|
2049
|
+
ctx.setdefault(
|
|
2050
|
+
"REPORT_WRITER_PREAMBLE_PATH",
|
|
2051
|
+
str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
|
|
2052
|
+
)
|
|
2053
|
+
ctx.setdefault(
|
|
2054
|
+
"WORKER_ERROR_CONTRACT_PATH",
|
|
2055
|
+
str(runtime_home / "templates" / "worker-error-contract.md"),
|
|
2056
|
+
)
|
|
2012
2057
|
# Lead resource paths the launch template references directly. paths.py
|
|
2013
2058
|
# seeds the production values; setdefault backfills callers that render the
|
|
2014
|
-
# template without the full path ctx
|
|
2059
|
+
# template without the full path ctx.
|
|
2015
2060
|
ctx.setdefault(
|
|
2016
2061
|
"OKSTRA_LEAD_CONTRACT_PATH",
|
|
2017
2062
|
str(runtime_home / "prompts" / "lead" / "okstra-lead-contract.md"),
|