okstra 0.77.0 → 0.78.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 (41) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/okstra +1 -125
  4. package/docs/contributor-change-matrix.md +12 -0
  5. package/docs/kr/cli.md +5 -4
  6. package/docs/project-structure-overview.md +1 -1
  7. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +493 -0
  8. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +134 -0
  9. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +284 -0
  10. package/package.json +5 -2
  11. package/runtime/BUILD.json +2 -2
  12. package/runtime/DO_NOT_EDIT.md +21 -0
  13. package/runtime/agents/SKILL.md +3 -0
  14. package/runtime/bin/okstra-trace-cleanup.sh +16 -12
  15. package/runtime/prompts/profiles/forbidden-actions.json +69 -0
  16. package/runtime/prompts/profiles/implementation.md +1 -8
  17. package/runtime/prompts/profiles/release-handoff.md +1 -15
  18. package/runtime/python/okstra_ctl/codex_dispatch.py +70 -2
  19. package/runtime/python/okstra_ctl/context_cost.py +1 -1
  20. package/runtime/python/okstra_ctl/dispatch_core.py +897 -0
  21. package/runtime/python/okstra_ctl/doctor.py +292 -0
  22. package/runtime/python/okstra_ctl/lead_runtime.py +72 -0
  23. package/runtime/python/okstra_ctl/render.py +67 -16
  24. package/runtime/python/okstra_ctl/run.py +20 -12
  25. package/runtime/python/okstra_ctl/team.py +267 -0
  26. package/runtime/python/okstra_ctl/tmux.py +181 -10
  27. package/runtime/python/okstra_ctl/workflow.py +30 -71
  28. package/runtime/python/okstra_ctl/wrapper_status.py +55 -0
  29. package/runtime/schemas/final-report-v1.0.schema.json +3 -1
  30. package/runtime/skills/okstra-convergence/SKILL.md +3 -0
  31. package/runtime/skills/okstra-report-writer/SKILL.md +3 -0
  32. package/runtime/skills/okstra-team-contract/SKILL.md +12 -0
  33. package/runtime/validators/forbidden_actions.py +135 -0
  34. package/runtime/validators/validate-run.py +65 -9
  35. package/runtime/validators/validate_session_conformance.py +15 -10
  36. package/src/cli-registry.mjs +277 -0
  37. package/src/doctor.mjs +93 -4
  38. package/src/install.mjs +6 -5
  39. package/src/runtime-manifest.mjs +5 -2
  40. package/src/team.mjs +63 -0
  41. package/src/uninstall.mjs +1 -0
@@ -19,6 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Mapping, Sequence
20
20
 
21
21
  from .lead_events import LeadEvent, append_lead_event
22
+ from .wrapper_status import status_path_for_prompt
22
23
  from .paths import task_dir
23
24
 
24
25
 
@@ -206,6 +207,7 @@ def dispatch_plan(plan: DispatchPlan) -> int:
206
207
 
207
208
  def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerDispatch) -> int:
208
209
  for attempt in range(1, MAX_WORKER_ATTEMPTS + 1):
210
+ _record_worker_attempt(plan, worker, attempt, "running")
209
211
  attempt_details = _attempt_event_details(worker, attempt)
210
212
  _append_event(plan, "worker-dispatched", attempt_details)
211
213
  result = _run_worker(plan, worker)
@@ -214,7 +216,8 @@ def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerDispatch) -> i
214
216
  post_process = _post_process_report_writer_result(plan, worker)
215
217
  if not post_process["ok"]:
216
218
  reason = _require_string(post_process, "reason")
217
- _set_worker_status(plan.team_state_path, worker.worker_id, "failed", reason)
219
+ _set_worker_status(plan.team_state_path, worker.worker_id, "error", reason)
220
+ _record_worker_attempt(plan, worker, attempt, "error", reason)
218
221
  details = {
219
222
  **attempt_details,
220
223
  "exitCode": 1,
@@ -225,6 +228,7 @@ def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerDispatch) -> i
225
228
  _append_event(plan, "worker-failed", details)
226
229
  return 1
227
230
  _set_worker_status(plan.team_state_path, worker.worker_id, "completed", "")
231
+ _record_worker_attempt(plan, worker, attempt, "completed")
228
232
  details = {
229
233
  **attempt_details,
230
234
  "missingCompletionPaths": [],
@@ -238,11 +242,19 @@ def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerDispatch) -> i
238
242
  "missingCompletionPaths": [str(path) for path in missing_paths],
239
243
  "reason": "required worker artifact was not produced",
240
244
  }
245
+ _record_worker_attempt(
246
+ plan,
247
+ worker,
248
+ attempt,
249
+ "error",
250
+ "required worker artifact was not produced",
251
+ )
241
252
  _append_event(plan, "worker-retry-scheduled", retry_details)
242
253
  continue
243
254
 
244
255
  reason = _failure_reason(result.returncode, missing_paths)
245
- _set_worker_status(plan.team_state_path, worker.worker_id, "failed", reason)
256
+ _set_worker_status(plan.team_state_path, worker.worker_id, "error", reason)
257
+ _record_worker_attempt(plan, worker, attempt, "error", reason)
246
258
  details = {
247
259
  **attempt_details,
248
260
  "exitCode": result.returncode,
@@ -252,6 +264,62 @@ def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerDispatch) -> i
252
264
  _append_event(plan, "worker-failed", details)
253
265
  return result.returncode if result.returncode else 1
254
266
  return 1
267
+ def _record_worker_attempt(
268
+ plan: DispatchPlan,
269
+ worker: WorkerDispatch,
270
+ attempt: int,
271
+ status: str,
272
+ reason: str = "",
273
+ ) -> None:
274
+ payload = _load_json_object(plan.team_state_path, "team-state")
275
+ dispatches = payload.setdefault("workerDispatches", [])
276
+ if not isinstance(dispatches, list):
277
+ raise DispatchError(
278
+ f"team-state workerDispatches must be an array: {plan.team_state_path}"
279
+ )
280
+ for record in dispatches:
281
+ if _matches_worker_attempt(record, worker, attempt):
282
+ record["status"] = status
283
+ record["reason"] = reason
284
+ _write_json(plan.team_state_path, payload)
285
+ return
286
+ dispatches.append(_worker_dispatch_record(worker, attempt, status, reason))
287
+ _write_json(plan.team_state_path, payload)
288
+
289
+
290
+ def _matches_worker_attempt(
291
+ record: object, worker: WorkerDispatch, attempt: int
292
+ ) -> bool:
293
+ return (
294
+ isinstance(record, dict)
295
+ and record.get("workerId") == worker.worker_id
296
+ and record.get("kind") == "initial"
297
+ and record.get("attempt") == attempt
298
+ )
299
+
300
+
301
+ def _worker_dispatch_record(
302
+ worker: WorkerDispatch, attempt: int, status: str, reason: str
303
+ ) -> dict[str, Any]:
304
+ provider = "codex" if worker.worker_id == REPORT_WRITER_WORKER_ID else worker.worker_id
305
+ return {
306
+ "workerId": worker.worker_id,
307
+ "role": worker.role,
308
+ "kind": "initial",
309
+ "attempt": attempt,
310
+ "backendType": worker.backend,
311
+ "provider": provider,
312
+ "status": status,
313
+ "paneId": "",
314
+ "promptPath": str(worker.prompt_path),
315
+ "resultPath": str(worker.result_path),
316
+ "workerResultPath": str(worker.worker_result_path),
317
+ "statusSidecarPath": str(status_path_for_prompt(worker.prompt_path)),
318
+ "degradedFrom": "",
319
+ "reason": reason,
320
+ }
321
+
322
+
255
323
 
256
324
 
257
325
  def main(argv: Sequence[str] | None = None) -> int:
@@ -188,7 +188,7 @@ def _skill_assets_metric() -> dict:
188
188
  for fname in WORKER_AGENT_FILES:
189
189
  path = _installed_or_dev(
190
190
  claude_home / "agents" / fname,
191
- f"agents/workers/{fname}",
191
+ f"runtime/agents/workers/{fname}",
192
192
  )
193
193
  entries.append((f"agent:{fname}", path))
194
194