okstra 0.30.3 → 0.32.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 (69) hide show
  1. package/docs/kr/architecture.md +2 -2
  2. package/docs/kr/cli.md +2 -2
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/agents/SKILL.md +7 -5
  6. package/runtime/agents/workers/claude-worker.md +1 -1
  7. package/runtime/agents/workers/codex-worker.md +23 -6
  8. package/runtime/agents/workers/gemini-worker.md +23 -6
  9. package/runtime/agents/workers/report-writer-worker.md +45 -66
  10. package/runtime/bin/okstra-codex-exec.sh +31 -0
  11. package/runtime/bin/okstra-gemini-exec.sh +26 -0
  12. package/runtime/bin/okstra-render-final-report.py +101 -0
  13. package/runtime/bin/okstra-render-report-views.py +17 -10
  14. package/runtime/bin/okstra-token-usage.py +3 -1
  15. package/runtime/python/lib/okstra/globals.sh +1 -1
  16. package/runtime/python/lib/okstra/usage.sh +2 -2
  17. package/runtime/python/okstra_ctl/final_report_schema.py +253 -0
  18. package/runtime/python/okstra_ctl/models.py +2 -0
  19. package/runtime/python/okstra_ctl/render_final_report.py +201 -0
  20. package/runtime/python/okstra_ctl/report_views.py +276 -297
  21. package/runtime/python/okstra_ctl/run.py +1 -1
  22. package/runtime/python/okstra_ctl/wizard.py +53 -14
  23. package/runtime/python/okstra_ctl/workers.py +45 -11
  24. package/runtime/python/okstra_token_usage/__init__.py +5 -1
  25. package/runtime/python/okstra_token_usage/cli.py +66 -36
  26. package/runtime/python/okstra_token_usage/pricing.py +1 -0
  27. package/runtime/python/okstra_token_usage/report.py +148 -65
  28. package/runtime/python/okstra_vendor/__init__.py +37 -0
  29. package/runtime/python/okstra_vendor/jinja2/__init__.py +38 -0
  30. package/runtime/python/okstra_vendor/jinja2/_identifier.py +6 -0
  31. package/runtime/python/okstra_vendor/jinja2/async_utils.py +99 -0
  32. package/runtime/python/okstra_vendor/jinja2/bccache.py +408 -0
  33. package/runtime/python/okstra_vendor/jinja2/compiler.py +1998 -0
  34. package/runtime/python/okstra_vendor/jinja2/constants.py +20 -0
  35. package/runtime/python/okstra_vendor/jinja2/debug.py +191 -0
  36. package/runtime/python/okstra_vendor/jinja2/defaults.py +48 -0
  37. package/runtime/python/okstra_vendor/jinja2/environment.py +1672 -0
  38. package/runtime/python/okstra_vendor/jinja2/exceptions.py +166 -0
  39. package/runtime/python/okstra_vendor/jinja2/ext.py +870 -0
  40. package/runtime/python/okstra_vendor/jinja2/filters.py +1873 -0
  41. package/runtime/python/okstra_vendor/jinja2/idtracking.py +318 -0
  42. package/runtime/python/okstra_vendor/jinja2/lexer.py +868 -0
  43. package/runtime/python/okstra_vendor/jinja2/loaders.py +693 -0
  44. package/runtime/python/okstra_vendor/jinja2/meta.py +112 -0
  45. package/runtime/python/okstra_vendor/jinja2/nativetypes.py +130 -0
  46. package/runtime/python/okstra_vendor/jinja2/nodes.py +1206 -0
  47. package/runtime/python/okstra_vendor/jinja2/optimizer.py +48 -0
  48. package/runtime/python/okstra_vendor/jinja2/parser.py +1049 -0
  49. package/runtime/python/okstra_vendor/jinja2/py.typed +0 -0
  50. package/runtime/python/okstra_vendor/jinja2/runtime.py +1062 -0
  51. package/runtime/python/okstra_vendor/jinja2/sandbox.py +436 -0
  52. package/runtime/python/okstra_vendor/jinja2/tests.py +256 -0
  53. package/runtime/python/okstra_vendor/jinja2/utils.py +766 -0
  54. package/runtime/python/okstra_vendor/jinja2/visitor.py +92 -0
  55. package/runtime/python/okstra_vendor/markupsafe/__init__.py +396 -0
  56. package/runtime/python/okstra_vendor/markupsafe/_native.py +8 -0
  57. package/runtime/python/okstra_vendor/markupsafe/py.typed +0 -0
  58. package/runtime/schemas/final-report-v1.0.schema.json +1391 -0
  59. package/runtime/skills/okstra-report-writer/SKILL.md +31 -30
  60. package/runtime/skills/okstra-run/SKILL.md +6 -4
  61. package/runtime/skills/okstra-team-contract/SKILL.md +27 -3
  62. package/runtime/templates/reports/final-report.template.md +370 -405
  63. package/runtime/templates/reports/report.css +57 -4
  64. package/runtime/templates/reports/report.js +63 -7
  65. package/runtime/templates/reports/settings.template.json +1 -0
  66. package/runtime/validators/lib/fixtures.sh +7 -7
  67. package/runtime/validators/validate-report-views.py +24 -153
  68. package/runtime/validators/validate-run.py +102 -19
  69. package/src/install.mjs +21 -1
@@ -10,6 +10,25 @@ import sys
10
10
  from datetime import datetime, timezone
11
11
  from pathlib import Path
12
12
 
13
+ # Make the in-tree ``okstra_ctl`` package importable when this validator
14
+ # runs from the repo (the installed runtime already has ~/.okstra/lib/python
15
+ # on PYTHONPATH so the import is a no-op there).
16
+ _VALIDATORS_DIR = Path(__file__).resolve().parent
17
+ _SCRIPTS_DIR = _VALIDATORS_DIR.parent / "scripts"
18
+ if _SCRIPTS_DIR.is_dir() and str(_SCRIPTS_DIR) not in sys.path:
19
+ sys.path.insert(0, str(_SCRIPTS_DIR))
20
+
21
+ try:
22
+ from okstra_ctl.final_report_schema import (
23
+ SchemaError,
24
+ load_schema,
25
+ validate as schema_validate,
26
+ )
27
+ except ImportError: # pragma: no cover — runtime guarantees this import
28
+ SchemaError = None # type: ignore[assignment]
29
+ load_schema = None # type: ignore[assignment]
30
+ schema_validate = None # type: ignore[assignment]
31
+
13
32
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
14
33
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
15
34
 
@@ -558,7 +577,7 @@ def _scan_token_usage_summary(content: str, failures: list[str]) -> None:
558
577
  "Token Usage Summary cell contains sentinel value "
559
578
  f"`{stripped}` on row labelled `{label_cell or '<unlabeled>'}` — "
560
579
  "leave the `{{...}}` placeholder verbatim until "
561
- "`okstra-token-usage.py --substitute-final-report` runs "
580
+ "`okstra-token-usage.py --substitute-data` runs "
562
581
  "in Phase 7."
563
582
  )
564
583
  continue
@@ -567,7 +586,7 @@ def _scan_token_usage_summary(content: str, failures: list[str]) -> None:
567
586
  f"Token Usage Summary row `{label_cell or '<unlabeled>'}` has "
568
587
  f"a zero value `{stripped}` — no okstra run consumes zero "
569
588
  "tokens. Re-run `python3 scripts/okstra-token-usage.py "
570
- "<team-state> --write --summary --substitute-final-report "
589
+ "<team-state> --write --summary --substitute-data "
571
590
  "<report-path>` to repopulate from session jsonls. The "
572
591
  "Codex/Gemini CLI row is the only place `$0.00` is "
573
592
  "allowed (when no CLI work was billed)."
@@ -664,7 +683,7 @@ def validate_report(
664
683
  if placeholder in content:
665
684
  failures.append(
666
685
  f"final report contains unsubstituted token placeholder `{placeholder}` — "
667
- "run `okstra-token-usage.py ... --substitute-final-report <report-path>` during Phase 7"
686
+ "run `okstra-token-usage.py ... --substitute-data <report-path>` during Phase 7"
668
687
  )
669
688
 
670
689
  # Catch the "workers typed `0` / `pending` instead of the placeholder"
@@ -794,7 +813,7 @@ def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
794
813
  failures.append(
795
814
  "team-state.usageSummary is empty — Phase 7 token-usage collection was skipped. "
796
815
  "Run `python3 scripts/okstra-token-usage.py <team-state> --write --summary "
797
- "--substitute-final-report <final-report>`."
816
+ "--substitute-data <final-report>`."
798
817
  )
799
818
  return
800
819
  # Reject zero-valued usage when the collector flagged any source as
@@ -973,11 +992,69 @@ def _validate_verdict_card_consistency(content: str, failures: list[str]) -> Non
973
992
  )
974
993
 
975
994
 
995
+ def _data_path_for(report_path: Path) -> Path:
996
+ """Derive the final-report data.json sibling from the markdown path.
997
+ ``foo.md`` → ``foo.data.json``. The data.json is the canonical JSON
998
+ SSOT; the markdown is rendered from it.
999
+ """
1000
+ name = report_path.name
1001
+ if name.endswith(".md"):
1002
+ return report_path.with_name(name[:-3] + ".data.json")
1003
+ return report_path.with_suffix(".data.json")
1004
+
1005
+
1006
+ def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
1007
+ """Validate the final-report data.json against the v1.0 schema.
1008
+
1009
+ The data.json is the source-of-truth that the renderer reads to
1010
+ produce the markdown. If schema validation passes here, the rendered
1011
+ markdown is guaranteed to contain every section / row the contract
1012
+ requires (the template loops over the data). The downstream
1013
+ substring checks in ``validate_phase_boundary`` are kept as a safety
1014
+ net but are expected to be redundant.
1015
+
1016
+ Missing data.json is reported as a single failure rather than a
1017
+ cascade of substring failures — that points the writer at the right
1018
+ fix (write the data.json) instead of futilely editing the markdown.
1019
+ """
1020
+ if schema_validate is None or load_schema is None:
1021
+ # Module-load fallback path; should never fire in a real install.
1022
+ failures.append(
1023
+ "validate-run: okstra_ctl.final_report_schema is not importable — "
1024
+ "install may be incomplete (scripts/ not on PYTHONPATH)."
1025
+ )
1026
+ return
1027
+
1028
+ data_path = _data_path_for(report_path)
1029
+ if not data_path.is_file():
1030
+ failures.append(
1031
+ f"final-report data.json is missing at {data_path} — the renderer "
1032
+ "needs this file as its single source of truth. The markdown "
1033
+ "alone is no longer a valid run artifact."
1034
+ )
1035
+ return
1036
+
1037
+ try:
1038
+ schema = load_schema()
1039
+ except SchemaError as exc:
1040
+ failures.append(f"final-report schema could not be loaded: {exc}")
1041
+ return
1042
+
1043
+ try:
1044
+ data = json.loads(data_path.read_text(encoding="utf-8"))
1045
+ except json.JSONDecodeError as exc:
1046
+ failures.append(f"final-report data.json is not valid JSON: {exc}")
1047
+ return
1048
+
1049
+ errors = schema_validate(data, schema)
1050
+ for err in errors:
1051
+ failures.append(f"final-report data.json: {err}")
1052
+
1053
+
976
1054
  def validate_report_views(report_path: Path, failures: list[str]) -> None:
977
- """Enforce Phase 7 step 1.5 (BLOCKING) — the slim AI copy and the
978
- self-contained HTML view must exist next to the final-report MD,
979
- and both must satisfy the contract checked by
980
- ``validators/validate-report-views.py``.
1055
+ """Enforce Phase 7 step 1.5 (BLOCKING) — the self-contained HTML
1056
+ view must exist next to the final-report MD and satisfy the
1057
+ contract checked by ``validators/validate-report-views.py``.
981
1058
 
982
1059
  Delegated to that script as a subprocess so the contract surface
983
1060
  stays in one place. Failures from the delegate are folded back as
@@ -1192,8 +1269,8 @@ def _import_token_usage():
1192
1269
  sys.path.insert(0, str(candidate))
1193
1270
  break
1194
1271
  from okstra_token_usage.collect import collect # noqa: E402
1195
- from okstra_token_usage.report import substitute_final_report # noqa: E402
1196
- return collect, substitute_final_report
1272
+ from okstra_token_usage.report import populate_and_render # noqa: E402
1273
+ return collect, populate_and_render
1197
1274
 
1198
1275
 
1199
1276
  def _needs_token_autofix(team_state: dict, report_path: Path) -> bool:
@@ -1286,7 +1363,7 @@ def attempt_token_usage_autofix(
1286
1363
  if not _needs_token_autofix(team_state, report_path):
1287
1364
  return "skipped", []
1288
1365
  try:
1289
- collect, substitute_final_report = _import_token_usage()
1366
+ collect, populate_and_render = _import_token_usage()
1290
1367
  except Exception as exc: # noqa: BLE001
1291
1368
  return "import-failed", [f"okstra_token_usage import failed: {exc}"]
1292
1369
  try:
@@ -1307,8 +1384,9 @@ def attempt_token_usage_autofix(
1307
1384
  team_state_path.write_text(
1308
1385
  json.dumps(updated, indent=2, ensure_ascii=False) + "\n"
1309
1386
  )
1387
+ data_path = _data_path_for(report_path)
1310
1388
  try:
1311
- replaced = substitute_final_report(report_path, updated)
1389
+ replaced, _bytes = populate_and_render(data_path, updated)
1312
1390
  except Exception as exc: # noqa: BLE001
1313
1391
  # `SubstituteRefusedError` (or any unexpected substitution
1314
1392
  # failure) — report it as an accuracy failure so the validator
@@ -1319,8 +1397,8 @@ def attempt_token_usage_autofix(
1319
1397
  ]
1320
1398
 
1321
1399
  # Phase 7 step 1.5 is BLOCKING and the autofix just mutated the
1322
- # source MD — any pre-existing slim/html sibling is now stale by
1323
- # construction. Re-render the derived views in lock-step so the
1400
+ # source MD — any pre-existing html sibling is now stale by
1401
+ # construction. Re-render the html view in lock-step so the
1324
1402
  # downstream report-views validator does not trip over the
1325
1403
  # autofix's own side effect.
1326
1404
  rerender_note = _rerender_report_views_after_autofix(report_path)
@@ -1339,9 +1417,9 @@ def attempt_token_usage_autofix(
1339
1417
 
1340
1418
 
1341
1419
  def _rerender_report_views_after_autofix(report_path: Path) -> str:
1342
- """Re-render ``*.slim.md`` and ``*.html`` siblings against the
1343
- just-substituted MD. Returns a short status note for the autofix
1344
- message (empty on no-op, descriptive on failure).
1420
+ """Re-render the ``*.html`` sibling against the just-substituted MD.
1421
+ Returns a short status note for the autofix message (empty on no-op,
1422
+ descriptive on failure).
1345
1423
  """
1346
1424
  if not report_path.is_file():
1347
1425
  return ""
@@ -1351,7 +1429,7 @@ def _rerender_report_views_after_autofix(report_path: Path) -> str:
1351
1429
  scripts_dir = Path(__file__).resolve().parent.parent / "scripts"
1352
1430
  if str(scripts_dir) not in sys.path:
1353
1431
  sys.path.insert(0, str(scripts_dir))
1354
- from okstra_ctl.report_views import RunMeta, render_both_views
1432
+ from okstra_ctl.report_views import RunMeta, render_html_view
1355
1433
  templates_dir = (
1356
1434
  Path(__file__).resolve().parent.parent / "templates" / "reports"
1357
1435
  )
@@ -1374,7 +1452,7 @@ def _rerender_report_views_after_autofix(report_path: Path) -> str:
1374
1452
  source_report=report_path.name,
1375
1453
  )
1376
1454
  try:
1377
- render_both_views(report_path, run_meta=meta, css=css, js=js)
1455
+ render_html_view(report_path, run_meta=meta, css=css, js=js)
1378
1456
  except Exception as exc: # noqa: BLE001
1379
1457
  return f"report-views re-render failed: {exc}"
1380
1458
  return "report-views re-rendered"
@@ -1445,6 +1523,11 @@ def main() -> int:
1445
1523
  failures.extend(autofix_messages)
1446
1524
  contract = extract_contract(run_manifest, task_manifest, failures)
1447
1525
  validate_team_state(team_state, project_root, contract, failures)
1526
+ # Schema validation runs BEFORE markdown substring checks: if the
1527
+ # data.json is well-formed, the rendered markdown is guaranteed to
1528
+ # contain every required section. Substring checks below are a
1529
+ # safety net for hand-edited or pre-v1.0 reports.
1530
+ validate_final_report_data(report_path, failures)
1448
1531
  validate_report(report_path, contract["required_agent_status_entries"], failures)
1449
1532
  validate_team_state_usage(team_state, failures)
1450
1533
 
package/src/install.mjs CHANGED
@@ -15,7 +15,7 @@ const SETTINGS_TEMPLATE_SRC_REL = ["templates", "reports", "settings.template.js
15
15
  // Destination under ~/.okstra/. Project-local .claude/settings.local.json symlinks here.
16
16
  const SETTINGS_TEMPLATE_DST_REL = ["templates", "settings.local.json"];
17
17
 
18
- const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "lib"];
18
+ const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "okstra_vendor", "lib"];
19
19
  const BIN_ENTRYPOINTS = [
20
20
  "okstra.sh",
21
21
  "okstra-codex-exec.sh",
@@ -25,6 +25,8 @@ const BIN_ENTRYPOINTS = [
25
25
  "okstra-token-usage.py",
26
26
  "okstra-error-log.py",
27
27
  "okstra-render-report-views.py",
28
+ "okstra-render-final-report.py",
29
+ "okstra-wrapper-status.py",
28
30
  ];
29
31
 
30
32
  const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
@@ -40,6 +42,7 @@ Usage:
40
42
  Effect (copy mode):
41
43
  ${"$HOME"}/.okstra/lib/python <- runtime/python
42
44
  ${"$HOME"}/.okstra/bin <- runtime/bin
45
+ ${"$HOME"}/.okstra/templates <- runtime/templates (report.css / report.js / *.template.md)
43
46
  ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
44
47
  ${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
45
48
  ${"$HOME"}/.claude/agents/<worker>.md <- runtime/agents/workers/<worker>.md
@@ -550,10 +553,22 @@ export async function runInstall(args) {
550
553
  paths.bin,
551
554
  { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 },
552
555
  );
556
+ // templates/ tree — report.css / report.js / *.template.md are consumed at
557
+ // runtime by okstra-render-report-views.py and final-report assembly. They
558
+ // are NOT covered by installSettingsTemplate (which only handles the
559
+ // editable settings.local.json sidecar), so without this step copy-mode
560
+ // installs miss every asset other than that single file. See
561
+ // okstra-render-report-views.py _TEMPLATES_DIRS for the lookup path.
562
+ const templatesResult = await copyTreeIfChanged(
563
+ join(runtimeRoot, "templates"),
564
+ join(paths.home, "templates"),
565
+ { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
566
+ );
553
567
 
554
568
  if (!opts.quiet) {
555
569
  summarise("python", pythonResult, paths.pythonpath);
556
570
  summarise("bin", binResult, paths.bin);
571
+ summarise("templates", templatesResult, join(paths.home, "templates"));
557
572
  }
558
573
 
559
574
  if (pythonResult.missingSource && binResult.missingSource) {
@@ -561,6 +576,11 @@ export async function runInstall(args) {
561
576
  "warning: runtime/{python,bin} are both empty. Runtime sync (build step) has not been performed.\n",
562
577
  );
563
578
  }
579
+ if (templatesResult.missingSource) {
580
+ process.stderr.write(
581
+ "warning: runtime/templates is empty. report.css / report.js will be missing — re-run the build step.\n",
582
+ );
583
+ }
564
584
 
565
585
  const skillResult = await installSkillsCopy(runtimeRoot, opts);
566
586
  await writeSkillsManifest(paths.home, skillResult.installed, { dryRun: opts.dryRun });