okstra 0.173.0 → 0.174.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 (62) hide show
  1. package/docs/architecture/storage-model.md +13 -3
  2. package/docs/architecture.md +5 -21
  3. package/docs/cli.md +3 -2
  4. package/docs/container.md +1 -1
  5. package/docs/contributor-change-matrix.md +1 -1
  6. package/docs/project-structure-overview.md +13 -13
  7. package/docs/task-process/README.md +1 -1
  8. package/docs/task-process/implementation-planning.md +1 -1
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/claude-worker.md +1 -1
  12. package/runtime/bin/lib/okstra/globals.sh +1 -1
  13. package/runtime/bin/okstra-provider-exec.py +29 -12
  14. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  15. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  16. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  17. package/runtime/prompts/lead/plan-body-verification.md +3 -3
  18. package/runtime/prompts/lead/report-writer.md +6 -6
  19. package/runtime/prompts/profiles/_common-contract.md +2 -2
  20. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  21. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  22. package/runtime/prompts/profiles/error-analysis.md +1 -1
  23. package/runtime/prompts/profiles/implementation-planning.md +12 -9
  24. package/runtime/prompts/profiles/implementation.md +2 -1
  25. package/runtime/prompts/profiles/release-handoff.md +1 -1
  26. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  27. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  28. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
  29. package/runtime/python/okstra_ctl/conformance.py +68 -0
  30. package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
  31. package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
  32. package/runtime/python/okstra_ctl/doctor.py +2 -2
  33. package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
  34. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  35. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  36. package/runtime/python/okstra_ctl/render_final_report.py +15 -19
  37. package/runtime/python/okstra_ctl/report_contract.py +0 -1
  38. package/runtime/python/okstra_ctl/report_finalize.py +68 -9
  39. package/runtime/python/okstra_ctl/run.py +43 -2
  40. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  41. package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
  42. package/runtime/python/okstra_ctl/session.py +69 -12
  43. package/runtime/python/okstra_ctl/team.py +51 -25
  44. package/runtime/python/okstra_ctl/tmux.py +19 -149
  45. package/runtime/python/okstra_ctl/worker_request.py +2 -0
  46. package/runtime/python/okstra_ctl/worktree.py +69 -3
  47. package/runtime/python/okstra_token_usage/cli.py +1 -1
  48. package/runtime/python/okstra_token_usage/collect.py +66 -6
  49. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  50. package/runtime/templates/reports/settings.template.json +0 -24
  51. package/runtime/validators/lib/fixtures.sh +49 -17
  52. package/runtime/validators/validate-implementation-plan-stages.py +63 -3
  53. package/runtime/validators/validate-run.py +14 -473
  54. package/runtime/validators/validate_session_conformance.py +1 -1
  55. package/src/cli-registry.mjs +8 -1
  56. package/src/commands/execute/team.mjs +3 -3
  57. package/src/commands/execute/worktree-status.mjs +109 -0
  58. package/src/commands/lifecycle/install.mjs +0 -2
  59. package/src/commands/report/finalize.mjs +13 -6
  60. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  61. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  62. package/runtime/templates/reports/final-report.template.md +0 -1258
@@ -52,36 +52,12 @@
52
52
  "SessionEnd": [
53
53
  {
54
54
  "hooks": [
55
- {
56
- "type": "command",
57
- "command": "$HOME/.okstra/bin/okstra-trace-cleanup.sh --reap"
58
- },
59
55
  {
60
56
  "type": "command",
61
57
  "command": "$HOME/.okstra/bin/okstra-team-reconcile.sh --session-end"
62
58
  }
63
59
  ]
64
60
  }
65
- ],
66
- "SubagentStop": [
67
- {
68
- "hooks": [
69
- {
70
- "type": "command",
71
- "command": "$HOME/.okstra/bin/okstra-subagent-reclaim.sh"
72
- }
73
- ]
74
- }
75
- ],
76
- "TaskCompleted": [
77
- {
78
- "hooks": [
79
- {
80
- "type": "command",
81
- "command": "$HOME/.okstra/bin/okstra-subagent-reclaim.sh"
82
- }
83
- ]
84
- }
85
61
  ]
86
62
  }
87
63
  }
@@ -428,7 +428,7 @@ if WORKSPACE_ROOT:
428
428
  task_type = str(task_manifest.get("taskType", ""))
429
429
  sample_path = (
430
430
  Path(WORKSPACE_ROOT)
431
- / "tests" / "fixtures" / "final-report-data"
431
+ / "tests" / "fixtures" / "final-report-data-v2"
432
432
  / f"{task_type}-001.data.json"
433
433
  )
434
434
  if sample_path.is_file():
@@ -439,6 +439,25 @@ if WORKSPACE_ROOT:
439
439
  sample["frontmatter"]["projectId"] = str(task_manifest.get("projectId", ""))
440
440
  sample["header"]["taskKey"] = str(task_manifest.get("taskKey", ""))
441
441
  sample["header"]["taskType"] = task_type
442
+ # The shipped fixture carries its own roster; this run's contract names
443
+ # a different one, and the validator compares the report's agent rows
444
+ # against that contract. Restate the rows under the contract's names so
445
+ # the fixture exercises the check instead of tripping over it.
446
+ _required = (
447
+ (run_manifest.get("teamContract") or {}).get("requiredAgentStatusEntries")
448
+ or (task_manifest.get("resultContract") or {}).get(
449
+ "requiredAgentStatusEntries"
450
+ )
451
+ or []
452
+ )
453
+ _rows = sample.get("executionStatus") or []
454
+ if _required and _rows:
455
+ # `agent` is the runtime (a schema enum); `role` carries the label the
456
+ # validator looks for in the rendered markdown.
457
+ sample["executionStatus"] = [
458
+ {**_rows[min(i, len(_rows) - 1)], "role": role}
459
+ for i, role in enumerate(_required)
460
+ ]
442
461
  name = report_path.name
443
462
  data_path = (
444
463
  report_path.with_name(name[:-3] + ".data.json")
@@ -452,23 +471,36 @@ if WORKSPACE_ROOT:
452
471
 
453
472
  import sys as _sys
454
473
  _sys.path.insert(0, str(Path(WORKSPACE_ROOT) / "scripts"))
455
- try:
456
- from okstra_ctl.report_views import RunMeta, render_html_view
457
- css = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.css").read_text(encoding="utf-8")
458
- js = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.js").read_text(encoding="utf-8")
459
- render_html_view(
460
- report_path,
461
- run_meta=RunMeta(
462
- task_key=str(task_manifest.get("taskKey", "validation/fixture")),
463
- task_type=str(task_manifest.get("taskType", "validation")),
464
- seq="001",
465
- source_report=report_path.name,
466
- ),
467
- css=css,
468
- js=js,
474
+ # The markdown seeded above this block predates the data.json contract, so
475
+ # re-render it from the data.json the fixture just wrote. Otherwise the pair
476
+ # disagrees and the validator's AI-handoff heading scan fails on a fixture
477
+ # that never claimed to be hand-authored.
478
+ if sample_path.is_file():
479
+ try:
480
+ from okstra_ctl.render_final_report import render_to_file
481
+
482
+ render_to_file(data_path, report_path)
483
+ except Exception as exc: # pragma: no cover — fixture path only
484
+ raise SystemExit(f"failed to render final report in fixture: {exc}")
485
+ # Go through the same CLI the run uses, so the fixture picks the schema's
486
+ # own view (v2 task template) rather than a second copy of that choice.
487
+ import subprocess as _subprocess
488
+ _views = _subprocess.run(
489
+ [
490
+ _sys.executable,
491
+ str(Path(WORKSPACE_ROOT) / "scripts" / "okstra-render-report-views.py"),
492
+ str(report_path),
493
+ "--task-key", str(task_manifest.get("taskKey", "validation/fixture")),
494
+ "--task-type", str(task_manifest.get("taskType", "validation")),
495
+ "--seq", "001",
496
+ ],
497
+ capture_output=True,
498
+ text=True,
499
+ )
500
+ if _views.returncode != 0:
501
+ raise SystemExit(
502
+ f"failed to render report views in fixture: {_views.stderr or _views.stdout}"
469
503
  )
470
- except Exception as exc: # pragma: no cover — fixture path only
471
- raise SystemExit(f"failed to render report views in fixture: {exc}")
472
504
 
473
505
  if final_status_path.exists():
474
506
  final_status_path.unlink()
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """S1–S11 checks for the Stage Map structure of an approved
2
+ """S1–S13 checks for the Stage Map structure of an approved
3
3
  implementation-planning final-report.md. Run from prepare_task_bundle
4
4
  of `implementation` task or standalone."""
5
5
 
@@ -43,9 +43,26 @@ EXIT_CONTRACT_HEADING = re.compile(r"^###\s+Stage Exit Contract\b", re.M)
43
43
  PATH_TOKEN = re.compile(r"(?:[\w.@-]+/)+[\w.@-]+")
44
44
 
45
45
 
46
+ # S12 — a step command that reads an okstra artifact back out of a git object
47
+ # (`git cat-file -e <rev>:.okstra/...`, `git show <rev>:.okstra/...`). `.okstra/**`
48
+ # is never committed: `_implementation-executor.md` forbids `git add -f` and makes
49
+ # a staged ignored path abort the commit, and `_implementation-verifier.md` reports
50
+ # a committed `.okstra` path as a branch defect. A verification step built on such
51
+ # a read can never pass, whatever the stage does.
52
+ GIT_OBJECT_OKSTRA_READ = re.compile(
53
+ r"\bgit\b[^&|;]*?\b(?:cat-file|show|ls-tree|archive|grep)\b[^&|;]*?"
54
+ r"(?<![\w./@'\"-])[\w./@{}~^-]+:\.okstra/"
55
+ )
56
+ # S13 — a clean-worktree assertion built on a bare `git status`. okstra provisions
57
+ # `.okstra`, the configured sync entries, and (for implementation) a nested stage
58
+ # worktree into every task worktree, so a bare status is never empty there.
59
+ BARE_GIT_STATUS = re.compile(r"\bgit\b[^&|;]*?\bstatus\b[^&|;]*?--(?:porcelain|short)\b")
60
+ CLEAN_GATE_COMMAND = "okstra worktree-status --check-clean"
61
+
62
+
46
63
  @dataclass
47
64
  class ValidationError:
48
- code: str # S1..S11
65
+ code: str # S1..S13
49
66
  stage: int # 0 = global
50
67
  message: str
51
68
 
@@ -92,10 +109,14 @@ def _slice_stage_section(text: str, stage_number: int) -> str:
92
109
  return text[start: start + nxt.start()] if nxt else text[start:]
93
110
 
94
111
 
112
+ STEP_COMMAND_CELL = 3
113
+
114
+
95
115
  def _effective_step_rows(section: str) -> List[List[str]]:
96
116
  """Effective (non header/divider/comment) rows of the `### Stepwise
97
117
  Execution Order` table, each as a list of stripped cells. Columns are
98
- `step | action | files | command | expected`, so action is index 1."""
118
+ `step | action | files | command | outcome | expected`, so action is
119
+ index 1, command index 3, outcome index 4."""
99
120
  m = re.search(r"^###\s+Stepwise Execution Order\b", section, re.M)
100
121
  if not m:
101
122
  return []
@@ -267,6 +288,40 @@ def _check_red_green_steps(section: str, stage_number: int) -> List[ValidationEr
267
288
  return errs
268
289
 
269
290
 
291
+ def _check_step_command(command: str, stage_number: int) -> List[ValidationError]:
292
+ """S12 / S13 over one step's `command` cell.
293
+
294
+ The command cell is what actually closes a step, so it has to be runnable
295
+ inside the worktree layout okstra provisions. Both rules reject a command
296
+ that can never pass there, regardless of what the stage implements.
297
+ """
298
+ errs: List[ValidationError] = []
299
+ if GIT_OBJECT_OKSTRA_READ.search(command):
300
+ errs.append(ValidationError("S12", stage_number,
301
+ "S12: step command reads an `.okstra/` path out of a git object — "
302
+ "`.okstra/**` is gitignored and never committed, so the read can "
303
+ "never resolve. Pass the artifact forward through the stage carry "
304
+ "sidecar / verifier result, or read it from the working tree"))
305
+ if BARE_GIT_STATUS.search(command):
306
+ errs.append(ValidationError("S13", stage_number,
307
+ "S13: step command asserts a clean worktree with a bare `git status` — "
308
+ "okstra provisions `.okstra`, the synced entries, and any nested stage "
309
+ f"worktree there, so it is never empty. Use `{CLEAN_GATE_COMMAND}`"))
310
+ return errs
311
+
312
+
313
+ def _check_markdown_step_commands(
314
+ text: str, stages: List[StageMapStage]
315
+ ) -> List[ValidationError]:
316
+ """S12 / S13 over the rendered `### Stepwise Execution Order` rows."""
317
+ errs: List[ValidationError] = []
318
+ for s in stages:
319
+ for row in _effective_step_rows(_slice_stage_section(text, s.stage_number)):
320
+ if len(row) > STEP_COMMAND_CELL:
321
+ errs.extend(_check_step_command(row[STEP_COMMAND_CELL], s.stage_number))
322
+ return errs
323
+
324
+
270
325
  def _check_conformance_declaration(
271
326
  text: str, stages: List[StageMapStage]
272
327
  ) -> List[ValidationError]:
@@ -383,6 +438,7 @@ def collect_validation_errors(text: str) -> List[ValidationError]:
383
438
  if stages:
384
439
  errors.extend(_check_each_stage_section(text, stages))
385
440
  errors.extend(_check_slice_tdd(text, stages))
441
+ errors.extend(_check_markdown_step_commands(text, stages))
386
442
  errors.extend(_check_conformance_declaration(text, stages))
387
443
  errors.extend(_check_depends_on(stages))
388
444
  errors.extend(_check_parallel_safety(text, stages))
@@ -605,6 +661,10 @@ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
605
661
  }))
606
662
  for stage in stages:
607
663
  errors.extend(_check_data_slice_tdd(stage))
664
+ number = stage.get("stage") if isinstance(stage.get("stage"), int) else 0
665
+ for step in stage.get("stepwiseExecution") or []:
666
+ if isinstance(step, dict):
667
+ errors.extend(_check_step_command(str(step.get("command") or ""), number))
608
668
  return errors
609
669
 
610
670