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.
- package/docs/architecture/storage-model.md +13 -3
- package/docs/architecture.md +5 -21
- package/docs/cli.md +3 -2
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +13 -13
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/implementation-planning.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -1
- package/runtime/bin/okstra-provider-exec.py +29 -12
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/plan-body-verification.md +3 -3
- package/runtime/prompts/lead/report-writer.md +6 -6
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +12 -9
- package/runtime/prompts/profiles/implementation.md +2 -1
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
- package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
- package/runtime/python/okstra_ctl/doctor.py +2 -2
- package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/render_final_report.py +15 -19
- package/runtime/python/okstra_ctl/report_contract.py +0 -1
- package/runtime/python/okstra_ctl/report_finalize.py +68 -9
- package/runtime/python/okstra_ctl/run.py +43 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/worker_request.py +2 -0
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/validators/lib/fixtures.sh +49 -17
- package/runtime/validators/validate-implementation-plan-stages.py +63 -3
- package/runtime/validators/validate-run.py +14 -473
- package/runtime/validators/validate_session_conformance.py +1 -1
- package/src/cli-registry.mjs +8 -1
- package/src/commands/execute/team.mjs +3 -3
- package/src/commands/execute/worktree-status.mjs +109 -0
- package/src/commands/lifecycle/install.mjs +0 -2
- package/src/commands/report/finalize.mjs +13 -6
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- 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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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–
|
|
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..
|
|
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
|
|
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
|
|