okstra 0.71.1 → 0.72.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 (33) hide show
  1. package/docs/kr/architecture.md +14 -1
  2. package/docs/kr/cli.md +7 -1
  3. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +1290 -0
  4. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +94 -0
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/SKILL.md +1 -1
  8. package/runtime/bin/lib/okstra/cli.sh +5 -1
  9. package/runtime/bin/lib/okstra/globals.sh +1 -0
  10. package/runtime/bin/lib/okstra/usage.sh +6 -1
  11. package/runtime/bin/okstra.sh +1 -0
  12. package/runtime/prompts/profiles/_implementation-executor.md +1 -1
  13. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  14. package/runtime/prompts/profiles/implementation-planning.md +2 -2
  15. package/runtime/prompts/wizard/prompts.ko.json +9 -0
  16. package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
  17. package/runtime/python/okstra_ctl/conformance.py +5 -0
  18. package/runtime/python/okstra_ctl/fix_cycles.py +172 -0
  19. package/runtime/python/okstra_ctl/render.py +45 -5
  20. package/runtime/python/okstra_ctl/run.py +93 -0
  21. package/runtime/python/okstra_ctl/run_context.py +15 -9
  22. package/runtime/python/okstra_ctl/wizard.py +64 -4
  23. package/runtime/python/okstra_token_usage/claude.py +64 -8
  24. package/runtime/python/okstra_token_usage/collect.py +30 -1
  25. package/runtime/schemas/final-report-v1.0.schema.json +25 -0
  26. package/runtime/skills/okstra-brief/SKILL.md +8 -0
  27. package/runtime/skills/okstra-report-writer/SKILL.md +2 -0
  28. package/runtime/skills/okstra-run/SKILL.md +2 -1
  29. package/runtime/templates/project-docs/task-index.template.md +1 -0
  30. package/runtime/templates/reports/final-report.template.md +14 -0
  31. package/runtime/validators/validate-run.py +81 -4
  32. package/runtime/validators/validate_session_conformance.py +7 -1
  33. package/src/render-bundle.mjs +4 -1
@@ -334,8 +334,29 @@ def effective_run_task_type(run_manifest: dict, task_manifest: dict) -> str:
334
334
  ).strip()
335
335
 
336
336
 
337
+ def _is_legal_concurrent_run_skip(
338
+ team_create: object, concurrent_run_authorized: bool
339
+ ) -> bool:
340
+ """prepare 가 run-manifest 에 동시-run 을 기록한 run 에서만, 렌더 게이트
341
+ ("Concurrent-run: no-team background")가 지시한 teamCreate skipped 형태를
342
+ legal 터미널 상태로 인정한다. lead 의 team-state 자기 선언만으로는(앵커
343
+ 없이) 열리지 않는다 — 선언이 아닌 prepare-측 사실이 강제 근거다."""
344
+ if not concurrent_run_authorized or not isinstance(team_create, dict):
345
+ return False
346
+ return (
347
+ team_create.get("attempted") is False
348
+ and str(team_create.get("status", "")).strip() == "skipped"
349
+ and str(team_create.get("reason", "")).strip() == "concurrent-run"
350
+ )
351
+
352
+
337
353
  def validate_team_state(
338
- team_state: dict, project_root: Path, contract: dict, failures: list[str]
354
+ team_state: dict,
355
+ project_root: Path,
356
+ contract: dict,
357
+ failures: list[str],
358
+ *,
359
+ concurrent_run_authorized: bool = False,
339
360
  ) -> None:
340
361
  artifacts = team_state.get("artifacts")
341
362
  if not isinstance(artifacts, dict):
@@ -377,12 +398,18 @@ def validate_team_state(
377
398
  )
378
399
  if any_dispatched:
379
400
  team_create = team_state.get("teamCreate")
380
- if not isinstance(team_create, dict) or not team_create.get("attempted"):
401
+ if _is_legal_concurrent_run_skip(team_create, concurrent_run_authorized):
402
+ pass
403
+ elif not isinstance(team_create, dict) or not team_create.get("attempted"):
381
404
  failures.append(
382
405
  "team-state.teamCreate.attempted must be true once any worker has "
383
406
  "been dispatched (status in completed/timeout/error/in-progress). "
384
407
  "Phase 3 (TeamCreate) was skipped — workers ran in-process without "
385
- "the Teams split-pane surface. See agents/SKILL.md Phase 3."
408
+ "the Teams split-pane surface. See agents/SKILL.md Phase 3. "
409
+ "(The no-team concurrent-run path is legal ONLY when the "
410
+ "run-manifest carries prepare-recorded `concurrentRun.detected: "
411
+ 'true` AND team-state records `teamCreate: { attempted: false, '
412
+ 'status: "skipped", reason: "concurrent-run" }`.)'
386
413
  )
387
414
  else:
388
415
  tc_status = str(team_create.get("status", "")).strip()
@@ -1342,6 +1369,19 @@ def _data_path_for(report_path: Path) -> Path:
1342
1369
  return report_path.with_suffix(".data.json")
1343
1370
 
1344
1371
 
1372
+ def _load_final_report_data(report_path: Path) -> dict:
1373
+ """Best-effort parse of the final-report data.json sibling. Returns {} when
1374
+ absent or unparseable — those conditions are already surfaced as failures by
1375
+ validate_final_report_data; this loader only feeds cross-field checks."""
1376
+ data_path = _data_path_for(report_path)
1377
+ if not data_path.is_file():
1378
+ return {}
1379
+ try:
1380
+ return json.loads(data_path.read_text(encoding="utf-8"))
1381
+ except (OSError, json.JSONDecodeError):
1382
+ return {}
1383
+
1384
+
1345
1385
  def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
1346
1386
  """Validate the final-report data.json against the v1.0 schema.
1347
1387
 
@@ -1721,6 +1761,31 @@ def _validate_improvement_discovery(
1721
1761
  failures.append(f"improvement-discovery: {err}")
1722
1762
 
1723
1763
 
1764
+ def _validate_fix_cycle(run_manifest: dict, data: dict, failures: list[str]) -> None:
1765
+ """Enforce: when the run-manifest carries a fixCycleId, the final-report
1766
+ data.json MUST contain a fixCycle block whose ``cycle`` matches it.
1767
+
1768
+ Direction is one-way: a fixCycle block present without a run-manifest
1769
+ fixCycleId is NOT rejected — same posture as the schema-optional block.
1770
+ This check owns only the run→report direction (no missing/mismatched
1771
+ block when the run is attached), never the reverse."""
1772
+ cycle_id = (run_manifest or {}).get("fixCycleId", "")
1773
+ if not cycle_id:
1774
+ return
1775
+ block = (data or {}).get("fixCycle")
1776
+ if not isinstance(block, dict):
1777
+ failures.append(
1778
+ f"fix-cycle: run-manifest fixCycleId={cycle_id} but data.json has "
1779
+ "no fixCycle block"
1780
+ )
1781
+ return
1782
+ if block.get("cycle") != cycle_id:
1783
+ failures.append(
1784
+ f"fix-cycle: data.json fixCycle.cycle={block.get('cycle')!r} does "
1785
+ f"not match run-manifest fixCycleId={cycle_id!r}"
1786
+ )
1787
+
1788
+
1724
1789
  def _validate_session_conformance(
1725
1790
  team_state: dict,
1726
1791
  team_state_path: Path,
@@ -2114,13 +2179,25 @@ def main() -> int:
2114
2179
  if autofix_state == "accuracy-failed":
2115
2180
  failures.extend(autofix_messages)
2116
2181
  contract = extract_contract(run_manifest, task_manifest, failures)
2117
- validate_team_state(team_state, project_root, contract, failures)
2182
+ concurrent_run_authorized = bool(
2183
+ (run_manifest.get("concurrentRun") or {}).get("detected")
2184
+ )
2185
+ validate_team_state(
2186
+ team_state,
2187
+ project_root,
2188
+ contract,
2189
+ failures,
2190
+ concurrent_run_authorized=concurrent_run_authorized,
2191
+ )
2118
2192
  # Schema validation runs BEFORE markdown substring checks: if the
2119
2193
  # data.json is well-formed, the rendered markdown is guaranteed to
2120
2194
  # contain every required section. Substring checks below are a
2121
2195
  # safety net for hand-edited or pre-v1.0 reports.
2122
2196
  task_type = effective_run_task_type(run_manifest, task_manifest)
2123
2197
  validate_final_report_data(report_path, failures)
2198
+ _validate_fix_cycle(
2199
+ run_manifest, _load_final_report_data(report_path), failures
2200
+ )
2124
2201
  validate_report(report_path, contract["required_agent_status_entries"], failures)
2125
2202
  validate_team_state_usage(team_state, failures)
2126
2203
 
@@ -271,7 +271,13 @@ def _check_progress_checkpoints(
271
271
  str(w.get("status", "")).strip() in _DISPATCHED_STATUSES for w in workers
272
272
  )
273
273
  require("phase-2-prompts", bool(workers), "before any Write to assigned prompt paths")
274
- require("phase-3-team-create", any_dispatched, "immediately before the TeamCreate call")
274
+ require(
275
+ "phase-3-team-create",
276
+ any_dispatched,
277
+ "immediately before the TeamCreate call — or, in the authorized no-team "
278
+ "concurrent-run path, the `phase-3-team-create skipped (concurrent-run)` "
279
+ "variant emitted right after recording teamCreate skipped in team-state",
280
+ )
275
281
  _check_worker_checkpoint_lines(by_phase, analysis_workers, errors)
276
282
  require(
277
283
  "phase-5.5-convergence",
@@ -18,11 +18,14 @@ Usage:
18
18
  [--gemini-model <m>] [--report-writer-model <m>] \\
19
19
  [--related-tasks <list>] [--base-ref <ref>] \\
20
20
  [--clarification-response <path>] [--work-category <cat>] \\
21
- [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>]
21
+ [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
22
+ [--fix-cycle <yes|no>]
22
23
 
23
24
  --stage implementation / final-verification only
24
25
  --stages release-handoff only: PR stage bundle csv (empty = whole-task)
25
26
  --pr-template-path release-handoff only
27
+ --fix-cycle entry phase only: record this re-entry of a done task as a
28
+ bug-fix cycle (yes opens/continues the cycle)
26
29
 
27
30
  release-handoff takes NO --task-brief (briefs belong to entry phases) —
28
31
  prepare generates the run's input document from the cited verification