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.
- package/docs/kr/architecture.md +2 -2
- package/docs/kr/cli.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +7 -5
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +23 -6
- package/runtime/agents/workers/gemini-worker.md +23 -6
- package/runtime/agents/workers/report-writer-worker.md +45 -66
- package/runtime/bin/okstra-codex-exec.sh +31 -0
- package/runtime/bin/okstra-gemini-exec.sh +26 -0
- package/runtime/bin/okstra-render-final-report.py +101 -0
- package/runtime/bin/okstra-render-report-views.py +17 -10
- package/runtime/bin/okstra-token-usage.py +3 -1
- package/runtime/python/lib/okstra/globals.sh +1 -1
- package/runtime/python/lib/okstra/usage.sh +2 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +253 -0
- package/runtime/python/okstra_ctl/models.py +2 -0
- package/runtime/python/okstra_ctl/render_final_report.py +201 -0
- package/runtime/python/okstra_ctl/report_views.py +276 -297
- package/runtime/python/okstra_ctl/run.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +53 -14
- package/runtime/python/okstra_ctl/workers.py +45 -11
- package/runtime/python/okstra_token_usage/__init__.py +5 -1
- package/runtime/python/okstra_token_usage/cli.py +66 -36
- package/runtime/python/okstra_token_usage/pricing.py +1 -0
- package/runtime/python/okstra_token_usage/report.py +148 -65
- package/runtime/python/okstra_vendor/__init__.py +37 -0
- package/runtime/python/okstra_vendor/jinja2/__init__.py +38 -0
- package/runtime/python/okstra_vendor/jinja2/_identifier.py +6 -0
- package/runtime/python/okstra_vendor/jinja2/async_utils.py +99 -0
- package/runtime/python/okstra_vendor/jinja2/bccache.py +408 -0
- package/runtime/python/okstra_vendor/jinja2/compiler.py +1998 -0
- package/runtime/python/okstra_vendor/jinja2/constants.py +20 -0
- package/runtime/python/okstra_vendor/jinja2/debug.py +191 -0
- package/runtime/python/okstra_vendor/jinja2/defaults.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/environment.py +1672 -0
- package/runtime/python/okstra_vendor/jinja2/exceptions.py +166 -0
- package/runtime/python/okstra_vendor/jinja2/ext.py +870 -0
- package/runtime/python/okstra_vendor/jinja2/filters.py +1873 -0
- package/runtime/python/okstra_vendor/jinja2/idtracking.py +318 -0
- package/runtime/python/okstra_vendor/jinja2/lexer.py +868 -0
- package/runtime/python/okstra_vendor/jinja2/loaders.py +693 -0
- package/runtime/python/okstra_vendor/jinja2/meta.py +112 -0
- package/runtime/python/okstra_vendor/jinja2/nativetypes.py +130 -0
- package/runtime/python/okstra_vendor/jinja2/nodes.py +1206 -0
- package/runtime/python/okstra_vendor/jinja2/optimizer.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/parser.py +1049 -0
- package/runtime/python/okstra_vendor/jinja2/py.typed +0 -0
- package/runtime/python/okstra_vendor/jinja2/runtime.py +1062 -0
- package/runtime/python/okstra_vendor/jinja2/sandbox.py +436 -0
- package/runtime/python/okstra_vendor/jinja2/tests.py +256 -0
- package/runtime/python/okstra_vendor/jinja2/utils.py +766 -0
- package/runtime/python/okstra_vendor/jinja2/visitor.py +92 -0
- package/runtime/python/okstra_vendor/markupsafe/__init__.py +396 -0
- package/runtime/python/okstra_vendor/markupsafe/_native.py +8 -0
- package/runtime/python/okstra_vendor/markupsafe/py.typed +0 -0
- package/runtime/schemas/final-report-v1.0.schema.json +1391 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +31 -30
- package/runtime/skills/okstra-run/SKILL.md +6 -4
- package/runtime/skills/okstra-team-contract/SKILL.md +27 -3
- package/runtime/templates/reports/final-report.template.md +370 -405
- package/runtime/templates/reports/report.css +57 -4
- package/runtime/templates/reports/report.js +63 -7
- package/runtime/templates/reports/settings.template.json +1 -0
- package/runtime/validators/lib/fixtures.sh +7 -7
- package/runtime/validators/validate-report-views.py +24 -153
- package/runtime/validators/validate-run.py +102 -19
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
978
|
-
|
|
979
|
-
|
|
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
|
|
1196
|
-
return collect,
|
|
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,
|
|
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 =
|
|
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
|
|
1323
|
-
# construction. Re-render 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
|
|
1343
|
-
|
|
1344
|
-
|
|
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,
|
|
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
|
-
|
|
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 });
|