okstra 0.128.0 → 0.129.1
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 +21 -5
- package/docs/cli.md +13 -2
- package/docs/project-structure-overview.md +22 -7
- 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 +6 -6
- 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 +22 -28
- package/runtime/prompts/lead/team-contract.md +28 -11
- 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 +70 -319
- 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/report_finalize.py +394 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +6 -2
- 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 +18 -0
- package/src/commands/execute/convergence.mjs +34 -0
- package/src/commands/report/finalize.mjs +65 -0
|
@@ -125,6 +125,57 @@ def _installed_or_dev(installed: Path, dev_relative: str) -> Path:
|
|
|
125
125
|
return Path(__file__).resolve().parents[2] / dev_relative
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def _runtime_template(filename: str) -> Path:
|
|
129
|
+
return _installed_or_dev(
|
|
130
|
+
Path.home() / ".okstra" / "templates" / filename,
|
|
131
|
+
f"templates/{filename}",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _header_path(prompt_path: Path | None, header: str) -> Path | None:
|
|
136
|
+
if prompt_path is None or not prompt_path.is_file():
|
|
137
|
+
return None
|
|
138
|
+
prefix = f"**{header}:**"
|
|
139
|
+
for line in prompt_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
140
|
+
stripped = line.strip()
|
|
141
|
+
if stripped.startswith(prefix):
|
|
142
|
+
value = stripped[len(prefix):].strip().strip("`")
|
|
143
|
+
return Path(value) if value else None
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _prompt_contract_paths(
|
|
148
|
+
prompt_path: Path | None,
|
|
149
|
+
default_preamble: str,
|
|
150
|
+
) -> tuple[Path, Path]:
|
|
151
|
+
selected_preamble = _header_path(prompt_path, "Worker Preamble Path")
|
|
152
|
+
selected_error = _header_path(prompt_path, "Worker Error Contract Path")
|
|
153
|
+
preamble = (
|
|
154
|
+
selected_preamble
|
|
155
|
+
if selected_preamble and selected_preamble.is_file()
|
|
156
|
+
else _runtime_template(
|
|
157
|
+
selected_preamble.name if selected_preamble else default_preamble
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
error_contract = (
|
|
161
|
+
selected_error
|
|
162
|
+
if selected_error and selected_error.is_file()
|
|
163
|
+
else _runtime_template("worker-error-contract.md")
|
|
164
|
+
)
|
|
165
|
+
return preamble, error_contract
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _initial_prompt(run_dir: Path | None, *, report_writer: bool) -> Path | None:
|
|
169
|
+
if run_dir is None:
|
|
170
|
+
return None
|
|
171
|
+
candidates = sorted((run_dir / "prompts").glob("*.md"))
|
|
172
|
+
for path in candidates:
|
|
173
|
+
is_report_writer = "report-writer" in path.name
|
|
174
|
+
if is_report_writer == report_writer and "-reverify-r" not in path.name:
|
|
175
|
+
return path
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
128
179
|
def _instruction_set_metric(task_root: Path, project_root: Path) -> dict:
|
|
129
180
|
instruction_set = task_root / "instruction-set"
|
|
130
181
|
files = _all_files(instruction_set)
|
|
@@ -233,21 +284,31 @@ def _lead_phase1_metric(
|
|
|
233
284
|
}
|
|
234
285
|
|
|
235
286
|
|
|
236
|
-
def _analysis_worker_metric(
|
|
287
|
+
def _analysis_worker_metric(
|
|
288
|
+
task_root: Path,
|
|
289
|
+
project_root: Path,
|
|
290
|
+
run_dir: Path | None,
|
|
291
|
+
task_type: str,
|
|
292
|
+
) -> dict:
|
|
237
293
|
instruction_set = task_root / "instruction-set"
|
|
238
294
|
full_contract_files = [instruction_set / name for name in INPUT_FILES]
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"
|
|
295
|
+
default_preamble = (
|
|
296
|
+
"implementation-worker-preamble.md"
|
|
297
|
+
if task_type == "implementation"
|
|
298
|
+
else "worker-prompt-preamble.md"
|
|
242
299
|
)
|
|
243
|
-
|
|
300
|
+
preamble, error_contract = _prompt_contract_paths(
|
|
301
|
+
_initial_prompt(run_dir, report_writer=False),
|
|
302
|
+
default_preamble,
|
|
303
|
+
)
|
|
304
|
+
full_contract_files.extend((preamble, error_contract))
|
|
244
305
|
full_file_count, full_byte_count = _count_files(full_contract_files)
|
|
245
306
|
|
|
246
307
|
packet = instruction_set / "analysis-packet.md"
|
|
247
308
|
legacy_packet = instruction_set / "task-packet.md"
|
|
248
309
|
primary_packet = packet if packet.is_file() else legacy_packet
|
|
249
310
|
has_packet = primary_packet.is_file()
|
|
250
|
-
packet_files = [primary_packet, preamble] if has_packet else []
|
|
311
|
+
packet_files = [primary_packet, preamble, error_contract] if has_packet else []
|
|
251
312
|
packet_file_count, packet_byte_count = _count_files(packet_files)
|
|
252
313
|
mode = "analysis-packet-primary" if packet.is_file() else "full-input-contract"
|
|
253
314
|
byte_count = packet_byte_count if packet.is_file() else full_byte_count
|
|
@@ -266,6 +327,8 @@ def _analysis_worker_metric(task_root: Path, project_root: Path) -> dict:
|
|
|
266
327
|
"legacyFullContractFileCount": full_file_count,
|
|
267
328
|
"estimatedPacketModeBytesPerWorker": packet_byte_count,
|
|
268
329
|
"estimatedReductionPercent": reduction_percent,
|
|
330
|
+
"promptPreamblePath": str(preamble),
|
|
331
|
+
"workerErrorContractPath": str(error_contract),
|
|
269
332
|
"files": [project_rel(path, project_root) for path in current_files if path.is_file()],
|
|
270
333
|
"legacyFullContractFiles": [
|
|
271
334
|
project_rel(path, project_root)
|
|
@@ -313,6 +376,11 @@ def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: P
|
|
|
313
376
|
instruction_set / "final-report-template.md",
|
|
314
377
|
instruction_set / "final-report-schema.json",
|
|
315
378
|
])
|
|
379
|
+
preamble, error_contract = _prompt_contract_paths(
|
|
380
|
+
_initial_prompt(run_dir, report_writer=True),
|
|
381
|
+
"report-writer-prompt-preamble.md",
|
|
382
|
+
)
|
|
383
|
+
files.extend((preamble, error_contract))
|
|
316
384
|
if run_dir is not None:
|
|
317
385
|
files.extend(_current_seq_worker_results(run_dir))
|
|
318
386
|
convergence = _latest_matching_file(
|
|
@@ -326,6 +394,8 @@ def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: P
|
|
|
326
394
|
"fileCount": file_count,
|
|
327
395
|
"bytes": byte_count,
|
|
328
396
|
"estimatedTokens": _estimate_tokens(files),
|
|
397
|
+
"promptPreamblePath": str(preamble),
|
|
398
|
+
"workerErrorContractPath": str(error_contract),
|
|
329
399
|
"files": [project_rel(path, project_root) for path in files if path.is_file()],
|
|
330
400
|
}
|
|
331
401
|
|
|
@@ -357,7 +427,12 @@ def analyze_task_bundle(task_root: Path, project_root: Path) -> dict:
|
|
|
357
427
|
},
|
|
358
428
|
"instructionSet": _instruction_set_metric(task_root, project_root),
|
|
359
429
|
"leadPhase1": _lead_phase1_metric(task_root, run_dir, manifest, project_root),
|
|
360
|
-
"analysisWorker": _analysis_worker_metric(
|
|
430
|
+
"analysisWorker": _analysis_worker_metric(
|
|
431
|
+
task_root,
|
|
432
|
+
project_root,
|
|
433
|
+
run_dir,
|
|
434
|
+
str(manifest.get("taskType") or ""),
|
|
435
|
+
),
|
|
361
436
|
"reportWriter": _report_writer_metric(run_dir, task_root, project_root),
|
|
362
437
|
"skillAssets": _skill_assets_metric(),
|
|
363
438
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""CLI orchestration for deterministic Phase 5.5 convergence state."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .convergence_engine import (
|
|
11
|
+
ConvergenceContractError,
|
|
12
|
+
apply_round_results,
|
|
13
|
+
finalize_working_state,
|
|
14
|
+
plan_next_round,
|
|
15
|
+
seed_working_state,
|
|
16
|
+
validate_final_state,
|
|
17
|
+
validate_working_state,
|
|
18
|
+
)
|
|
19
|
+
from .convergence_migration import (
|
|
20
|
+
SeedDecision,
|
|
21
|
+
archive_state_bytes,
|
|
22
|
+
decide_seed_action,
|
|
23
|
+
migration_record,
|
|
24
|
+
)
|
|
25
|
+
from .convergence_store import (
|
|
26
|
+
load_json_object,
|
|
27
|
+
write_final_state_atomic,
|
|
28
|
+
write_json_atomic,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parser() -> argparse.ArgumentParser:
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
prog="okstra convergence",
|
|
35
|
+
description="Advance and validate deterministic re-verification state.",
|
|
36
|
+
)
|
|
37
|
+
subparsers = parser.add_subparsers(dest="operation", required=True)
|
|
38
|
+
|
|
39
|
+
seed = subparsers.add_parser("seed", help="create, resume, or recover working state")
|
|
40
|
+
seed.add_argument("--groups", type=Path, required=True)
|
|
41
|
+
seed.add_argument("--work-state", type=Path, required=True)
|
|
42
|
+
seed.add_argument("--final-state", type=Path, required=True)
|
|
43
|
+
seed.add_argument("--migration-dir", type=Path, required=True)
|
|
44
|
+
seed.add_argument("--restart-from-round0", action="store_true")
|
|
45
|
+
|
|
46
|
+
plan = subparsers.add_parser("plan-round", help="write the next dispatch plan")
|
|
47
|
+
plan.add_argument("--work-state", type=Path, required=True)
|
|
48
|
+
plan.add_argument("--plan", type=Path, required=True)
|
|
49
|
+
|
|
50
|
+
apply = subparsers.add_parser("apply-round", help="apply structured round results")
|
|
51
|
+
apply.add_argument("--work-state", type=Path, required=True)
|
|
52
|
+
apply.add_argument("--plan", type=Path, required=True)
|
|
53
|
+
apply.add_argument("--results", type=Path, required=True)
|
|
54
|
+
|
|
55
|
+
finalize = subparsers.add_parser("finalize", help="write the public final state")
|
|
56
|
+
finalize.add_argument("--work-state", type=Path, required=True)
|
|
57
|
+
finalize.add_argument("--output", type=Path, required=True)
|
|
58
|
+
|
|
59
|
+
validate = subparsers.add_parser("validate", help="validate a persisted state")
|
|
60
|
+
validate.add_argument("--state", type=Path, required=True)
|
|
61
|
+
validate.add_argument("--kind", choices=("working", "final"), required=True)
|
|
62
|
+
return parser
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _emit(operation: str, action: str, path: Path) -> None:
|
|
66
|
+
print(
|
|
67
|
+
json.dumps(
|
|
68
|
+
{
|
|
69
|
+
"ok": True,
|
|
70
|
+
"operation": operation,
|
|
71
|
+
"action": action,
|
|
72
|
+
"path": str(path),
|
|
73
|
+
},
|
|
74
|
+
ensure_ascii=False,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _archive_existing_states(
|
|
80
|
+
*,
|
|
81
|
+
work_state_path: Path,
|
|
82
|
+
final_state_path: Path,
|
|
83
|
+
migration_dir: Path,
|
|
84
|
+
) -> dict[str, str] | None:
|
|
85
|
+
records: list[dict[str, str]] = []
|
|
86
|
+
for path in (work_state_path, final_state_path):
|
|
87
|
+
if not path.exists():
|
|
88
|
+
continue
|
|
89
|
+
archive_path, digest = archive_state_bytes(
|
|
90
|
+
source_path=path,
|
|
91
|
+
migration_dir=migration_dir,
|
|
92
|
+
)
|
|
93
|
+
records.append(migration_record(path, archive_path, digest))
|
|
94
|
+
if not records:
|
|
95
|
+
return None
|
|
96
|
+
final_record = next(
|
|
97
|
+
(record for record in records if record["sourcePath"] == str(final_state_path)),
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
return final_record or records[0]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _resume_after_legacy_final(
|
|
104
|
+
decision: SeedDecision,
|
|
105
|
+
*,
|
|
106
|
+
work_state_path: Path,
|
|
107
|
+
final_state_path: Path,
|
|
108
|
+
migration_dir: Path,
|
|
109
|
+
) -> None:
|
|
110
|
+
if decision.archive_path is None:
|
|
111
|
+
return
|
|
112
|
+
archive_path, digest = archive_state_bytes(
|
|
113
|
+
source_path=final_state_path,
|
|
114
|
+
migration_dir=migration_dir,
|
|
115
|
+
)
|
|
116
|
+
state = load_json_object(work_state_path)
|
|
117
|
+
state["migration"] = migration_record(final_state_path, archive_path, digest)
|
|
118
|
+
write_json_atomic(work_state_path, state)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _seed(args: argparse.Namespace) -> tuple[str, Path]:
|
|
122
|
+
grouped_input = load_json_object(args.groups)
|
|
123
|
+
decision = decide_seed_action(
|
|
124
|
+
grouped_input=grouped_input,
|
|
125
|
+
work_state_path=args.work_state,
|
|
126
|
+
final_state_path=args.final_state,
|
|
127
|
+
restart_from_round0=args.restart_from_round0,
|
|
128
|
+
)
|
|
129
|
+
if decision.action == "reuse-final":
|
|
130
|
+
return decision.action, args.final_state
|
|
131
|
+
if decision.action == "resume-work":
|
|
132
|
+
_resume_after_legacy_final(
|
|
133
|
+
decision,
|
|
134
|
+
work_state_path=args.work_state,
|
|
135
|
+
final_state_path=args.final_state,
|
|
136
|
+
migration_dir=args.migration_dir,
|
|
137
|
+
)
|
|
138
|
+
return decision.action, args.work_state
|
|
139
|
+
|
|
140
|
+
state = seed_working_state(grouped_input)
|
|
141
|
+
if decision.action == "restart-round0":
|
|
142
|
+
migration = _archive_existing_states(
|
|
143
|
+
work_state_path=args.work_state,
|
|
144
|
+
final_state_path=args.final_state,
|
|
145
|
+
migration_dir=args.migration_dir,
|
|
146
|
+
)
|
|
147
|
+
if migration is None:
|
|
148
|
+
raise ConvergenceContractError("restart-round0 has no state to archive")
|
|
149
|
+
state["migration"] = migration
|
|
150
|
+
write_json_atomic(args.work_state, state)
|
|
151
|
+
return decision.action, args.work_state
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _plan_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
155
|
+
state = load_json_object(args.work_state)
|
|
156
|
+
errors = validate_working_state(state)
|
|
157
|
+
if errors:
|
|
158
|
+
raise ConvergenceContractError("invalid working state: " + "; ".join(errors))
|
|
159
|
+
plan = plan_next_round(state)
|
|
160
|
+
write_json_atomic(args.plan, plan)
|
|
161
|
+
return str(plan["action"]), args.plan
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _apply_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
165
|
+
state = load_json_object(args.work_state)
|
|
166
|
+
plan = load_json_object(args.plan)
|
|
167
|
+
results = load_json_object(args.results)
|
|
168
|
+
updated = apply_round_results(state, plan, results)
|
|
169
|
+
errors = validate_working_state(updated)
|
|
170
|
+
if errors:
|
|
171
|
+
raise ConvergenceContractError("invalid updated working state: " + "; ".join(errors))
|
|
172
|
+
write_json_atomic(args.work_state, updated)
|
|
173
|
+
return "applied", args.work_state
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _finalize(args: argparse.Namespace) -> tuple[str, Path]:
|
|
177
|
+
state = load_json_object(args.work_state)
|
|
178
|
+
final = finalize_working_state(state)
|
|
179
|
+
migration = state.get("migration")
|
|
180
|
+
write_final_state_atomic(args.output, final, migration=migration)
|
|
181
|
+
return "finalized", args.output
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _validate(args: argparse.Namespace) -> tuple[str, Path]:
|
|
185
|
+
state = load_json_object(args.state)
|
|
186
|
+
errors = (
|
|
187
|
+
validate_working_state(state)
|
|
188
|
+
if args.kind == "working"
|
|
189
|
+
else validate_final_state(state)
|
|
190
|
+
)
|
|
191
|
+
if errors:
|
|
192
|
+
raise ConvergenceContractError("; ".join(errors))
|
|
193
|
+
return "valid", args.state
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _execute(args: argparse.Namespace) -> tuple[str, Path]:
|
|
197
|
+
operations: dict[str, Any] = {
|
|
198
|
+
"seed": _seed,
|
|
199
|
+
"plan-round": _plan_round,
|
|
200
|
+
"apply-round": _apply_round,
|
|
201
|
+
"finalize": _finalize,
|
|
202
|
+
"validate": _validate,
|
|
203
|
+
}
|
|
204
|
+
return operations[args.operation](args)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main(argv: list[str] | None = None) -> int:
|
|
208
|
+
args = _parser().parse_args(argv)
|
|
209
|
+
try:
|
|
210
|
+
action, path = _execute(args)
|
|
211
|
+
except (ConvergenceContractError, json.JSONDecodeError, ValueError) as exc:
|
|
212
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
213
|
+
return 2
|
|
214
|
+
except OSError as exc:
|
|
215
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
216
|
+
return 1
|
|
217
|
+
_emit(args.operation, action, path)
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
raise SystemExit(main())
|
|
223
|
+
|