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.
Files changed (47) hide show
  1. package/docs/architecture/storage-model.md +15 -2
  2. package/docs/architecture.md +19 -3
  3. package/docs/cli.md +8 -0
  4. package/docs/project-structure-overview.md +14 -5
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/antigravity-worker.md +3 -3
  8. package/runtime/agents/workers/claude-worker.md +4 -4
  9. package/runtime/agents/workers/codex-worker.md +3 -3
  10. package/runtime/agents/workers/report-writer-worker.md +5 -5
  11. package/runtime/prompts/lead/adapters/claude-code.md +2 -1
  12. package/runtime/prompts/lead/adapters/codex.md +1 -0
  13. package/runtime/prompts/lead/adapters/external.md +1 -0
  14. package/runtime/prompts/lead/convergence.md +42 -84
  15. package/runtime/prompts/lead/okstra-lead-contract.md +10 -8
  16. package/runtime/prompts/lead/report-writer.md +3 -2
  17. package/runtime/prompts/lead/team-contract.md +11 -9
  18. package/runtime/prompts/profiles/_common-contract.md +1 -1
  19. package/runtime/prompts/profiles/error-analysis.md +1 -1
  20. package/runtime/prompts/profiles/implementation-planning.md +5 -0
  21. package/runtime/prompts/profiles/improvement-discovery.md +11 -1
  22. package/runtime/prompts/profiles/requirements-discovery.md +8 -1
  23. package/runtime/python/okstra_ctl/analysis_packet.py +7 -13
  24. package/runtime/python/okstra_ctl/codex_dispatch.py +42 -97
  25. package/runtime/python/okstra_ctl/context_cost.py +82 -7
  26. package/runtime/python/okstra_ctl/convergence.py +223 -0
  27. package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
  28. package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
  29. package/runtime/python/okstra_ctl/convergence_store.py +85 -0
  30. package/runtime/python/okstra_ctl/dispatch_core.py +53 -14
  31. package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
  32. package/runtime/python/okstra_ctl/path_hints.py +23 -1
  33. package/runtime/python/okstra_ctl/paths.py +10 -1
  34. package/runtime/python/okstra_ctl/render.py +56 -11
  35. package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
  36. package/runtime/python/okstra_ctl/worker_prompt_contract.py +102 -9
  37. package/runtime/python/okstra_ctl/worker_prompt_headers.py +79 -9
  38. package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
  39. package/runtime/templates/implementation-worker-preamble.md +51 -0
  40. package/runtime/templates/operating-standard.md +4 -4
  41. package/runtime/templates/report-writer-prompt-preamble.md +37 -0
  42. package/runtime/templates/worker-error-contract.md +46 -0
  43. package/runtime/templates/worker-prompt-preamble.md +28 -234
  44. package/runtime/validators/lib/fixtures.sh +27 -12
  45. package/runtime/validators/validate-run.py +201 -72
  46. package/src/cli-registry.mjs +7 -0
  47. package/src/commands/execute/convergence.mjs +34 -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(task_root: Path, project_root: Path) -> dict:
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
- preamble = _installed_or_dev(
240
- Path.home() / ".okstra" / "templates" / "worker-prompt-preamble.md",
241
- "templates/worker-prompt-preamble.md",
295
+ default_preamble = (
296
+ "implementation-worker-preamble.md"
297
+ if task_type == "implementation"
298
+ else "worker-prompt-preamble.md"
242
299
  )
243
- full_contract_files.append(preamble)
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(task_root, project_root),
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
+