okstra 0.31.0 → 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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +45 -67
- 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/okstra_ctl/final_report_schema.py +253 -0
- package/runtime/python/okstra_ctl/render_final_report.py +201 -0
- package/runtime/python/okstra_ctl/report_views.py +108 -305
- 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/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 +29 -28
- package/runtime/templates/reports/final-report.template.md +370 -411
- package/runtime/templates/reports/report.css +12 -6
- 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 +20 -1
|
@@ -118,13 +118,19 @@ th, td {
|
|
|
118
118
|
word-break: keep-all;
|
|
119
119
|
overflow-wrap: anywhere;
|
|
120
120
|
min-width: 5ch;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
/* No max-width: cells that carry prose (statement / source / scope)
|
|
122
|
+
* absorb the remaining horizontal space within `main`'s 120ch. */
|
|
123
|
+
}
|
|
124
|
+
/* Narrow columns — header AND every body cell of this column fit
|
|
125
|
+
* within 30 plain chars (per `_narrow_columns()` in report_views.py).
|
|
126
|
+
* Examples: ID, Ticket ID, Kind, Status, Priority, Auto-spawn?,
|
|
127
|
+
* 에이전트, 역할, 모델, 상태. Pinning `width: 5%` makes these
|
|
128
|
+
* columns shrink toward the suggestion while wide neighbours absorb
|
|
129
|
+
* the remainder, so a long-prose row never squashes ID-style stacks
|
|
130
|
+
* into one-character ladders. */
|
|
131
|
+
td.td-narrow, th.td-narrow {
|
|
132
|
+
width: 5%;
|
|
126
133
|
white-space: nowrap;
|
|
127
|
-
width: 1%; /* shrink-to-fit under auto-layout */
|
|
128
134
|
}
|
|
129
135
|
thead th {
|
|
130
136
|
position: sticky;
|
|
@@ -366,21 +366,21 @@ report_lines.extend(
|
|
|
366
366
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
367
367
|
report_path.write_text("\n".join(report_lines) + "\n")
|
|
368
368
|
|
|
369
|
-
# Phase 7 step 1.5 (BLOCKING) — render the
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
369
|
+
# Phase 7 step 1.5 (BLOCKING) — render the html sibling artifact next
|
|
370
|
+
# to the final-report so validate-run.py's report-views hook passes.
|
|
371
|
+
# The workflow validator's fixture predates that step; we materialise
|
|
372
|
+
# the html file in-place using the same single-reference-point helper
|
|
373
|
+
# the CLI uses.
|
|
374
374
|
import os
|
|
375
375
|
WORKSPACE_ROOT = os.environ.get("OKSTRA_WORKSPACE_ROOT_FOR_FIXTURE", "")
|
|
376
376
|
if WORKSPACE_ROOT:
|
|
377
377
|
import sys as _sys
|
|
378
378
|
_sys.path.insert(0, str(Path(WORKSPACE_ROOT) / "scripts"))
|
|
379
379
|
try:
|
|
380
|
-
from okstra_ctl.report_views import RunMeta,
|
|
380
|
+
from okstra_ctl.report_views import RunMeta, render_html_view
|
|
381
381
|
css = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.css").read_text(encoding="utf-8")
|
|
382
382
|
js = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.js").read_text(encoding="utf-8")
|
|
383
|
-
|
|
383
|
+
render_html_view(
|
|
384
384
|
report_path,
|
|
385
385
|
run_meta=RunMeta(
|
|
386
386
|
task_key=str(task_manifest.get("taskKey", "validation/fixture")),
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Validate the
|
|
3
|
-
|
|
2
|
+
"""Validate the self-contained HTML view produced by Phase 7 step 1.5
|
|
3
|
+
(``scripts/okstra-render-report-views.py``).
|
|
4
4
|
|
|
5
5
|
Checks, for a given final-report MD path:
|
|
6
|
-
1. ``*.
|
|
7
|
-
2.
|
|
8
|
-
|
|
9
|
-
3. HTML
|
|
10
|
-
substrings (after html.unescape and the markdown ``#`` prefix is
|
|
11
|
-
stripped).
|
|
12
|
-
4. HTML's §4.6 / §4.7 / §4.8 deliverable regions contain no
|
|
6
|
+
1. ``*.html`` sibling exists.
|
|
7
|
+
2. HTML's ``source-sha256`` in run-meta matches the current MD body —
|
|
8
|
+
stale html detection.
|
|
9
|
+
3. HTML's §4.6 / §4.7 / §4.8 deliverable regions contain no
|
|
13
10
|
``<textarea>`` / ``<input>`` / ``<select>`` (form-attach is
|
|
14
11
|
restricted to §5 clarification rows).
|
|
15
|
-
|
|
12
|
+
4. HTML has no external URLs in ``<script src=>`` / ``<link href=>`` /
|
|
16
13
|
``<img src=>`` — self-contained guarantee.
|
|
17
|
-
|
|
14
|
+
5. Every Response ID in HTML matches the §5 Clarification Items table
|
|
18
15
|
of the source MD (1:1).
|
|
19
16
|
|
|
20
17
|
Exit codes: 0 on success, 1 on any failure. Failures are printed one
|
|
@@ -23,12 +20,9 @@ per line to stderr.
|
|
|
23
20
|
from __future__ import annotations
|
|
24
21
|
|
|
25
22
|
import argparse
|
|
26
|
-
import html as html_lib
|
|
27
|
-
import importlib.util
|
|
28
23
|
import re
|
|
29
24
|
import sys
|
|
30
25
|
from pathlib import Path
|
|
31
|
-
from typing import Iterable
|
|
32
26
|
|
|
33
27
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
34
28
|
SCRIPTS_DIR = REPO_ROOT / "scripts"
|
|
@@ -37,31 +31,17 @@ if str(SCRIPTS_DIR) not in sys.path:
|
|
|
37
31
|
|
|
38
32
|
from okstra_ctl.clarification_items import parse_clarification_items # noqa: E402
|
|
39
33
|
from okstra_ctl.report_views import ( # noqa: E402
|
|
40
|
-
_strip_leading_digest_comment,
|
|
41
34
|
extract_html_digest,
|
|
42
|
-
extract_slim_digest,
|
|
43
|
-
preserved_substrings,
|
|
44
|
-
slim_markdown,
|
|
45
35
|
source_digest,
|
|
46
36
|
)
|
|
47
37
|
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
here rather than silently in production."""
|
|
54
|
-
path = REPO_ROOT / "validators" / "validate-run.py"
|
|
55
|
-
spec = importlib.util.spec_from_file_location("_okstra_validate_run", path)
|
|
56
|
-
assert spec and spec.loader, f"cannot load {path}"
|
|
57
|
-
mod = importlib.util.module_from_spec(spec)
|
|
58
|
-
spec.loader.exec_module(mod)
|
|
59
|
-
return mod
|
|
60
|
-
|
|
39
|
+
_EXTERNAL_URL_RE = re.compile(
|
|
40
|
+
r'<(?:script|link|img|iframe|source|video|audio)\s[^>]*?(?:src|href)\s*=\s*["\']https?://',
|
|
41
|
+
re.IGNORECASE,
|
|
42
|
+
)
|
|
61
43
|
|
|
62
|
-
|
|
63
|
-
m = re.search(r"^- Task Type:\s*(\S+)", md, re.MULTILINE)
|
|
64
|
-
return m.group(1).strip() if m else None
|
|
44
|
+
_RESPONSE_ID_ATTR_RE = re.compile(r'data-response-id="(C-\d+)"')
|
|
65
45
|
|
|
66
46
|
|
|
67
47
|
def _main_body(html_text: str) -> str:
|
|
@@ -72,25 +52,6 @@ def _main_body(html_text: str) -> str:
|
|
|
72
52
|
return html_text[start + len("<main>"): end]
|
|
73
53
|
|
|
74
54
|
|
|
75
|
-
def _strip_md_prefix(sub: str) -> str:
|
|
76
|
-
s = sub
|
|
77
|
-
while s.startswith("#"):
|
|
78
|
-
s = s[1:]
|
|
79
|
-
return s.strip()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
_EXTERNAL_URL_RE = re.compile(
|
|
83
|
-
r'<(?:script|link|img|iframe|source|video|audio)\s[^>]*?(?:src|href)\s*=\s*["\']https?://',
|
|
84
|
-
re.IGNORECASE,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
_RESPONSE_ID_ATTR_RE = re.compile(r'data-response-id="(C-\d+)"')
|
|
88
|
-
|
|
89
|
-
_NO_FORM_HEADING_RE = re.compile(
|
|
90
|
-
r"<h[23][^>]*>\s*(?:4\.6|4\.7|4\.8)[\s\S]*?</h[23]>"
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
55
|
def _no_form_sections(html_body: str) -> list[str]:
|
|
95
56
|
"""Return a list of strings, each being the rendered chunk of a
|
|
96
57
|
no-form section (4.6 / 4.7 / 4.8) up to the next h2/h3. Used to
|
|
@@ -118,50 +79,21 @@ def validate(report_path: Path) -> list[str]:
|
|
|
118
79
|
return [f"final-report not found: {report_path}"]
|
|
119
80
|
|
|
120
81
|
md = report_path.read_text(encoding="utf-8")
|
|
121
|
-
task_type = _detect_task_type(md)
|
|
122
|
-
slim_path = report_path.with_name(report_path.stem + ".slim.md")
|
|
123
82
|
html_path = report_path.with_name(report_path.stem + ".html")
|
|
124
83
|
|
|
125
|
-
# (1) sibling
|
|
126
|
-
if not slim_path.is_file():
|
|
127
|
-
failures.append(f"missing slim artifact: {slim_path}")
|
|
84
|
+
# (1) sibling artifact exists
|
|
128
85
|
if not html_path.is_file():
|
|
129
|
-
|
|
130
|
-
if failures:
|
|
131
|
-
return failures
|
|
86
|
+
return [f"missing html artifact: {html_path}"]
|
|
132
87
|
|
|
133
|
-
slim = slim_path.read_text(encoding="utf-8")
|
|
134
88
|
html_text = html_path.read_text(encoding="utf-8")
|
|
135
89
|
html_body = _main_body(html_text)
|
|
136
|
-
html_body_text = html_lib.unescape(html_body)
|
|
137
90
|
|
|
138
|
-
# (
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
# leading digest comment), so the expected value is the digest
|
|
144
|
-
# of the current MD itself.
|
|
145
|
-
# - slim digest is over the slim BODY (so any byte-difference
|
|
146
|
-
# between expected and actual slim shows up as a digest
|
|
147
|
-
# mismatch). We recompute the expected slim and extract its
|
|
148
|
-
# embedded digest for comparison — equivalent to but cheaper
|
|
149
|
-
# than diffing the full body.
|
|
150
|
-
expected_md_digest = source_digest(_strip_leading_digest_comment(md))
|
|
151
|
-
expected_slim_digest = extract_slim_digest(
|
|
152
|
-
slim_markdown(md, task_type=task_type)
|
|
153
|
-
)
|
|
154
|
-
slim_digest = extract_slim_digest(slim)
|
|
91
|
+
# (2) source-digest staleness — html's run-meta source-sha256 must
|
|
92
|
+
# match the current MD's digest. Mismatch means the html was
|
|
93
|
+
# rendered against an older MD body and Phase 7 step 1.5 was not
|
|
94
|
+
# re-run.
|
|
95
|
+
expected_md_digest = source_digest(md)
|
|
155
96
|
html_digest = extract_html_digest(html_text)
|
|
156
|
-
if slim_digest is None:
|
|
157
|
-
failures.append(
|
|
158
|
-
"slim missing source-sha256 header — re-render with okstra render-views"
|
|
159
|
-
)
|
|
160
|
-
elif slim_digest != expected_slim_digest:
|
|
161
|
-
failures.append(
|
|
162
|
-
f"stale slim: body sha256 {slim_digest[:12]}… does not match "
|
|
163
|
-
f"current MD's rendered slim {str(expected_slim_digest)[:12]}…"
|
|
164
|
-
)
|
|
165
97
|
if html_digest is None:
|
|
166
98
|
failures.append(
|
|
167
99
|
"html missing source-sha256 in run-meta — re-render with okstra render-views"
|
|
@@ -172,31 +104,7 @@ def validate(report_path: Path) -> list[str]:
|
|
|
172
104
|
f"current MD {expected_md_digest[:12]}…"
|
|
173
105
|
)
|
|
174
106
|
|
|
175
|
-
# (
|
|
176
|
-
# must agree with validate-run.py's tuples for the same task_type.
|
|
177
|
-
vr = _load_validate_run()
|
|
178
|
-
drift = _diff_substring_lists(task_type, vr)
|
|
179
|
-
failures.extend(drift)
|
|
180
|
-
|
|
181
|
-
# (3) slim preserves every required substring byte-identically
|
|
182
|
-
for sub in preserved_substrings(task_type):
|
|
183
|
-
if sub in md and sub not in slim:
|
|
184
|
-
failures.append(f"slim dropped required substring: {sub!r}")
|
|
185
|
-
# Also forward-check against validate-run.py's tuples so a
|
|
186
|
-
# future addition there fails this validator immediately.
|
|
187
|
-
for sub in _validate_run_substrings(task_type, vr):
|
|
188
|
-
if sub in md and sub not in slim:
|
|
189
|
-
failures.append(f"slim dropped validate-run substring: {sub!r}")
|
|
190
|
-
|
|
191
|
-
# (4) html preserves the same set (modulo md prefix + html.escape)
|
|
192
|
-
for sub in preserved_substrings(task_type):
|
|
193
|
-
if sub not in md:
|
|
194
|
-
continue
|
|
195
|
-
needle = _strip_md_prefix(sub)
|
|
196
|
-
if needle not in html_body_text:
|
|
197
|
-
failures.append(f"html body dropped substring: {needle!r}")
|
|
198
|
-
|
|
199
|
-
# (5) deliverable sections contain no form controls
|
|
107
|
+
# (3) deliverable sections contain no form controls
|
|
200
108
|
for chunk in _no_form_sections(html_body):
|
|
201
109
|
if "<textarea" in chunk or "<input" in chunk or "<select" in chunk:
|
|
202
110
|
failures.append(
|
|
@@ -204,11 +112,11 @@ def validate(report_path: Path) -> list[str]:
|
|
|
204
112
|
)
|
|
205
113
|
break
|
|
206
114
|
|
|
207
|
-
# (
|
|
115
|
+
# (4) no external URLs in <script src> / <link href> / etc.
|
|
208
116
|
if _EXTERNAL_URL_RE.search(html_text):
|
|
209
117
|
failures.append("html contains external URL in script/link/img — must be self-contained")
|
|
210
118
|
|
|
211
|
-
# (
|
|
119
|
+
# (5) Response ID parity: HTML form rows ↔ §5 C-* rows in MD.
|
|
212
120
|
# Bidirectional — catches both "MD has C-* the HTML lost" AND
|
|
213
121
|
# "HTML has stale C-* that the current MD no longer declares".
|
|
214
122
|
md_ids = _md_response_ids(md)
|
|
@@ -221,43 +129,6 @@ def validate(report_path: Path) -> list[str]:
|
|
|
221
129
|
return failures
|
|
222
130
|
|
|
223
131
|
|
|
224
|
-
def _validate_run_substrings(task_type: str | None, vr) -> Iterable[str]:
|
|
225
|
-
if task_type == "implementation":
|
|
226
|
-
return getattr(vr, "IMPLEMENTATION_REQUIRED_SECTIONS", ())
|
|
227
|
-
if task_type == "final-verification":
|
|
228
|
-
return getattr(vr, "FINAL_VERIFICATION_REQUIRED_SECTIONS", ())
|
|
229
|
-
return ()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _diff_substring_lists(task_type: str | None, vr) -> list[str]:
|
|
233
|
-
"""Surface drift between report_views and validate-run substring
|
|
234
|
-
tuples. The two MUST stay in lock-step."""
|
|
235
|
-
failures: list[str] = []
|
|
236
|
-
from okstra_ctl.report_views import (
|
|
237
|
-
PRESERVED_SUBSTRINGS_IMPLEMENTATION,
|
|
238
|
-
PRESERVED_SUBSTRINGS_FINAL_VERIFICATION,
|
|
239
|
-
)
|
|
240
|
-
impl_vr = set(getattr(vr, "IMPLEMENTATION_REQUIRED_SECTIONS", ()))
|
|
241
|
-
impl_rv = set(PRESERVED_SUBSTRINGS_IMPLEMENTATION)
|
|
242
|
-
if impl_vr != impl_rv:
|
|
243
|
-
missing_in_views = impl_vr - impl_rv
|
|
244
|
-
extra_in_views = impl_rv - impl_vr
|
|
245
|
-
failures.append(
|
|
246
|
-
"drift: PRESERVED_SUBSTRINGS_IMPLEMENTATION vs validate-run "
|
|
247
|
-
f"IMPLEMENTATION_REQUIRED_SECTIONS — missing in views {missing_in_views or '∅'}, "
|
|
248
|
-
f"extra in views {extra_in_views or '∅'}"
|
|
249
|
-
)
|
|
250
|
-
fv_vr = set(getattr(vr, "FINAL_VERIFICATION_REQUIRED_SECTIONS", ()))
|
|
251
|
-
fv_rv = set(PRESERVED_SUBSTRINGS_FINAL_VERIFICATION)
|
|
252
|
-
if fv_vr != fv_rv:
|
|
253
|
-
failures.append(
|
|
254
|
-
"drift: PRESERVED_SUBSTRINGS_FINAL_VERIFICATION vs validate-run "
|
|
255
|
-
f"FINAL_VERIFICATION_REQUIRED_SECTIONS — missing in views {fv_vr - fv_rv or '∅'}, "
|
|
256
|
-
f"extra in views {fv_rv - fv_vr or '∅'}"
|
|
257
|
-
)
|
|
258
|
-
return failures
|
|
259
|
-
|
|
260
|
-
|
|
261
132
|
def _md_response_ids(md: str) -> list[str]:
|
|
262
133
|
items = parse_clarification_items(md) or []
|
|
263
134
|
return sorted({it.row_id for it in items if re.fullmatch(r"C-\d+", it.row_id)})
|
|
@@ -265,7 +136,7 @@ def _md_response_ids(md: str) -> list[str]:
|
|
|
265
136
|
|
|
266
137
|
def main(argv: list[str] | None = None) -> int:
|
|
267
138
|
parser = argparse.ArgumentParser(
|
|
268
|
-
description="Validate
|
|
139
|
+
description="Validate the self-contained HTML view of an okstra final-report."
|
|
269
140
|
)
|
|
270
141
|
parser.add_argument(
|
|
271
142
|
"report_path",
|
|
@@ -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,7 @@ 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",
|
|
28
29
|
"okstra-wrapper-status.py",
|
|
29
30
|
];
|
|
30
31
|
|
|
@@ -41,6 +42,7 @@ Usage:
|
|
|
41
42
|
Effect (copy mode):
|
|
42
43
|
${"$HOME"}/.okstra/lib/python <- runtime/python
|
|
43
44
|
${"$HOME"}/.okstra/bin <- runtime/bin
|
|
45
|
+
${"$HOME"}/.okstra/templates <- runtime/templates (report.css / report.js / *.template.md)
|
|
44
46
|
${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
|
|
45
47
|
${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
|
|
46
48
|
${"$HOME"}/.claude/agents/<worker>.md <- runtime/agents/workers/<worker>.md
|
|
@@ -551,10 +553,22 @@ export async function runInstall(args) {
|
|
|
551
553
|
paths.bin,
|
|
552
554
|
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 },
|
|
553
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
|
+
);
|
|
554
567
|
|
|
555
568
|
if (!opts.quiet) {
|
|
556
569
|
summarise("python", pythonResult, paths.pythonpath);
|
|
557
570
|
summarise("bin", binResult, paths.bin);
|
|
571
|
+
summarise("templates", templatesResult, join(paths.home, "templates"));
|
|
558
572
|
}
|
|
559
573
|
|
|
560
574
|
if (pythonResult.missingSource && binResult.missingSource) {
|
|
@@ -562,6 +576,11 @@ export async function runInstall(args) {
|
|
|
562
576
|
"warning: runtime/{python,bin} are both empty. Runtime sync (build step) has not been performed.\n",
|
|
563
577
|
);
|
|
564
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
|
+
}
|
|
565
584
|
|
|
566
585
|
const skillResult = await installSkillsCopy(runtimeRoot, opts);
|
|
567
586
|
await writeSkillsManifest(paths.home, skillResult.installed, { dryRun: opts.dryRun });
|