okstra 0.27.0 → 0.29.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 (28) hide show
  1. package/bin/okstra +1 -0
  2. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +167 -0
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/agents/workers/claude-worker.md +6 -5
  6. package/runtime/agents/workers/codex-worker.md +5 -4
  7. package/runtime/agents/workers/gemini-worker.md +5 -4
  8. package/runtime/agents/workers/report-writer-worker.md +10 -3
  9. package/runtime/bin/okstra-render-report-views.py +129 -0
  10. package/runtime/prompts/launch.template.md +1 -1
  11. package/runtime/prompts/profiles/_common-contract.md +12 -4
  12. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  13. package/runtime/python/okstra_ctl/report_views.py +701 -0
  14. package/runtime/python/okstra_token_usage/cli.py +9 -2
  15. package/runtime/python/okstra_token_usage/report.py +32 -3
  16. package/runtime/skills/okstra-convergence/SKILL.md +2 -2
  17. package/runtime/skills/okstra-report-writer/SKILL.md +25 -8
  18. package/runtime/skills/okstra-team-contract/SKILL.md +16 -15
  19. package/runtime/templates/reports/final-report.template.md +398 -211
  20. package/runtime/templates/reports/report.css +151 -0
  21. package/runtime/templates/reports/report.js +163 -0
  22. package/runtime/templates/reports/user-response.template.md +69 -0
  23. package/runtime/validators/lib/fixtures.sh +76 -2
  24. package/runtime/validators/validate-report-views.py +283 -0
  25. package/runtime/validators/validate-run.py +564 -4
  26. package/runtime/validators/validate-workflow.sh +4 -0
  27. package/src/install.mjs +1 -0
  28. package/src/render-views.mjs +67 -0
@@ -0,0 +1,151 @@
1
+ /* Single self-contained stylesheet for the okstra final-report HTML view.
2
+ * Inlined verbatim by scripts/okstra_ctl/report_views.py render_html.
3
+ * No external @import, no url() references. System colors only so dark
4
+ * mode follows the user's OS without a media query toggle.
5
+ */
6
+
7
+ * { box-sizing: border-box; }
8
+
9
+ html { color-scheme: light dark; }
10
+
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
15
+ font-size: 15px;
16
+ line-height: 1.55;
17
+ background: Canvas;
18
+ color: CanvasText;
19
+ }
20
+
21
+ .report-header,
22
+ .report-footer {
23
+ position: sticky;
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.75rem;
27
+ padding: 0.6rem 1rem;
28
+ background: Canvas;
29
+ border-bottom: 1px solid GrayText;
30
+ z-index: 10;
31
+ }
32
+
33
+ .report-header { top: 0; }
34
+ .report-footer {
35
+ bottom: 0;
36
+ border-top: 1px solid GrayText;
37
+ border-bottom: none;
38
+ flex-wrap: wrap;
39
+ }
40
+
41
+ .report-header > div { flex: 1; font-weight: 600; }
42
+
43
+ main {
44
+ max-width: 80ch;
45
+ margin: 1.5rem auto;
46
+ padding: 0 1rem 4rem;
47
+ }
48
+
49
+ h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin-top: 1.6em; margin-bottom: 0.4em; }
50
+ h1 { font-size: 1.7rem; }
51
+ h2 { font-size: 1.35rem; border-bottom: 1px solid GrayText; padding-bottom: 0.2em; }
52
+ h3 { font-size: 1.12rem; }
53
+ h4 { font-size: 1rem; }
54
+
55
+ p, ul, ol, blockquote, pre, table { margin: 0.6em 0; }
56
+
57
+ ul, ol { padding-left: 1.4em; }
58
+
59
+ blockquote {
60
+ border-left: 3px solid GrayText;
61
+ padding: 0.2em 0.8em;
62
+ color: GrayText;
63
+ }
64
+
65
+ code {
66
+ font-family: "SFMono-Regular", Menlo, Consolas, monospace;
67
+ font-size: 0.92em;
68
+ padding: 0.1em 0.35em;
69
+ background: color-mix(in srgb, CanvasText 8%, transparent);
70
+ border-radius: 3px;
71
+ }
72
+
73
+ pre {
74
+ overflow-x: auto;
75
+ padding: 0.8em 1em;
76
+ background: color-mix(in srgb, CanvasText 6%, transparent);
77
+ border-radius: 4px;
78
+ }
79
+ pre code { background: transparent; padding: 0; }
80
+
81
+ table {
82
+ width: 100%;
83
+ border-collapse: collapse;
84
+ font-size: 0.92rem;
85
+ }
86
+ th, td {
87
+ border: 1px solid color-mix(in srgb, GrayText 50%, transparent);
88
+ padding: 0.45em 0.6em;
89
+ vertical-align: top;
90
+ text-align: left;
91
+ }
92
+ thead th {
93
+ position: sticky;
94
+ top: 3rem;
95
+ background: Canvas;
96
+ z-index: 5;
97
+ }
98
+
99
+ tr[data-response-id] {
100
+ background: color-mix(in srgb, Highlight 6%, transparent);
101
+ }
102
+ tr[data-response-id][data-status="resolved"],
103
+ tr[data-response-id][data-status="obsolete"] {
104
+ background: transparent;
105
+ opacity: 0.65;
106
+ }
107
+
108
+ textarea {
109
+ width: 100%;
110
+ min-height: 2.2em;
111
+ font: inherit;
112
+ padding: 0.3em 0.4em;
113
+ border: 1px solid GrayText;
114
+ border-radius: 3px;
115
+ background: Canvas;
116
+ color: CanvasText;
117
+ resize: vertical;
118
+ }
119
+ textarea[disabled] { opacity: 0.55; }
120
+
121
+ button[data-action] {
122
+ font: inherit;
123
+ padding: 0.4em 0.9em;
124
+ border: 1px solid GrayText;
125
+ border-radius: 4px;
126
+ background: ButtonFace;
127
+ color: ButtonText;
128
+ cursor: pointer;
129
+ }
130
+ button[data-action]:hover { background: color-mix(in srgb, Highlight 20%, ButtonFace); }
131
+
132
+ #user-response-output {
133
+ flex-basis: 100%;
134
+ max-height: 14em;
135
+ overflow: auto;
136
+ margin: 0.6em 0 0;
137
+ padding: 0.6em 0.8em;
138
+ background: color-mix(in srgb, CanvasText 6%, transparent);
139
+ border-radius: 4px;
140
+ white-space: pre-wrap;
141
+ font-family: "SFMono-Regular", Menlo, Consolas, monospace;
142
+ font-size: 0.85rem;
143
+ }
144
+ #user-response-output:empty { display: none; }
145
+
146
+ @media print {
147
+ .report-header, .report-footer { position: static; }
148
+ thead th { position: static; }
149
+ button[data-action] { display: none; }
150
+ #user-response-output { display: none; }
151
+ }
@@ -0,0 +1,163 @@
1
+ /* Client-side glue for the okstra final-report HTML view.
2
+ *
3
+ * Responsibilities:
4
+ * 1. Collect <textarea> values for every <tr data-response-id> whose
5
+ * Status is open/answered (disabled rows are skipped automatically).
6
+ * 2. Serialise them into markdown whose bytes are IDENTICAL to
7
+ * scripts/okstra_ctl/report_views.py serialize_user_response.
8
+ * 3. Write the result to <pre id="user-response-output"> and offer a
9
+ * [Copy] button.
10
+ *
11
+ * The byte-identity contract is enforced by tests/test_report_views.py
12
+ * which spawns Node to execute buildUserResponseMarkdown and diffs the
13
+ * output against the Python function. If you edit the format here you
14
+ * MUST edit serialize_user_response too. The template at
15
+ * templates/reports/user-response.template.md documents the schema.
16
+ */
17
+
18
+ (function () {
19
+ "use strict";
20
+
21
+ function readRunMeta() {
22
+ var el = document.getElementById("run-meta");
23
+ if (!el) return {};
24
+ try {
25
+ return JSON.parse(el.textContent || "{}");
26
+ } catch (e) {
27
+ return {};
28
+ }
29
+ }
30
+
31
+ function isoNowUtc() {
32
+ return new Date().toISOString().replace(/\.\d+Z$/, "Z");
33
+ }
34
+
35
+ function trimMultiline(s) {
36
+ return String(s == null ? "" : s).replace(/^\s+|\s+$/g, "");
37
+ }
38
+
39
+ function collectEntries() {
40
+ var entries = [];
41
+ var rows = document.querySelectorAll("tr[data-response-id]");
42
+ for (var i = 0; i < rows.length; i++) {
43
+ var row = rows[i];
44
+ var ta = row.querySelector("textarea[data-response-id]");
45
+ if (!ta || ta.disabled) continue;
46
+ var value = trimMultiline(ta.value);
47
+ if (!value) continue;
48
+ entries.push({
49
+ responseId: row.getAttribute("data-response-id") || "",
50
+ kind: row.getAttribute("data-kind") || "",
51
+ value: value,
52
+ rationale: null,
53
+ });
54
+ }
55
+ return entries;
56
+ }
57
+
58
+ function buildUserResponseMarkdown(runMeta, entries, createdAt) {
59
+ var head =
60
+ "---\n" +
61
+ "task-key: " + (runMeta["task-key"] || "") + "\n" +
62
+ "task-type: " + (runMeta["task-type"] || "") + "\n" +
63
+ "seq: " + (runMeta["seq"] || "") + "\n" +
64
+ "source-report: " + (runMeta["source-report"] || "") + "\n" +
65
+ "created-by: user\n" +
66
+ "created-at: " + createdAt + "\n" +
67
+ "---\n" +
68
+ "\n" +
69
+ "# User Response\n";
70
+
71
+ if (!entries || entries.length === 0) {
72
+ return head + "\n_(No user responses recorded.)_\n";
73
+ }
74
+
75
+ var chunks = "";
76
+ for (var i = 0; i < entries.length; i++) {
77
+ var e = entries[i];
78
+ var chunk =
79
+ "\n## " + e.responseId + "\n" +
80
+ "- Kind: " + e.kind + "\n" +
81
+ "- Value:\n" +
82
+ " > " + trimMultiline(e.value) + "\n";
83
+ if (e.rationale) {
84
+ chunk += "- Rationale: " + trimMultiline(e.rationale) + "\n";
85
+ }
86
+ chunks += chunk;
87
+ }
88
+ return head + chunks;
89
+ }
90
+
91
+ function exportUserResponse() {
92
+ var runMeta = readRunMeta();
93
+ var entries = collectEntries();
94
+ var md = buildUserResponseMarkdown(runMeta, entries, isoNowUtc());
95
+ var out = document.getElementById("user-response-output");
96
+ if (out) out.textContent = md;
97
+ return md;
98
+ }
99
+
100
+ function copyUserResponse() {
101
+ var out = document.getElementById("user-response-output");
102
+ if (!out || !out.textContent) return;
103
+ var text = out.textContent;
104
+ if (navigator.clipboard && navigator.clipboard.writeText) {
105
+ navigator.clipboard.writeText(text).catch(function () {
106
+ fallbackCopy(text);
107
+ });
108
+ } else {
109
+ fallbackCopy(text);
110
+ }
111
+ }
112
+
113
+ function fallbackCopy(text) {
114
+ var ta = document.createElement("textarea");
115
+ ta.value = text;
116
+ ta.style.position = "fixed";
117
+ ta.style.opacity = "0";
118
+ document.body.appendChild(ta);
119
+ ta.select();
120
+ try { document.execCommand("copy"); } catch (e) {}
121
+ document.body.removeChild(ta);
122
+ }
123
+
124
+ function bind() {
125
+ var clickables = document.querySelectorAll("button[data-action]");
126
+ for (var i = 0; i < clickables.length; i++) {
127
+ var btn = clickables[i];
128
+ var action = btn.getAttribute("data-action");
129
+ if (action === "export-user-response") {
130
+ btn.addEventListener("click", exportUserResponse);
131
+ } else if (action === "copy-user-response") {
132
+ btn.addEventListener("click", copyUserResponse);
133
+ }
134
+ }
135
+ }
136
+
137
+ if (typeof window !== "undefined") {
138
+ if (document.readyState === "loading") {
139
+ document.addEventListener("DOMContentLoaded", bind);
140
+ } else {
141
+ bind();
142
+ }
143
+ // Expose for tests / debug.
144
+ window.okstraReportView = {
145
+ buildUserResponseMarkdown: buildUserResponseMarkdown,
146
+ collectEntries: collectEntries,
147
+ exportUserResponse: exportUserResponse,
148
+ };
149
+ }
150
+
151
+ // Node export for cross-impl byte-identity test. We expose on both
152
+ // CommonJS module.exports (works in `require()` callers) and
153
+ // globalThis (works under ESM where the parent uses `vm.runInThisContext`
154
+ // — see tests/test_report_views.py for the byte-identity harness).
155
+ if (typeof module !== "undefined" && module.exports) {
156
+ module.exports = { buildUserResponseMarkdown: buildUserResponseMarkdown };
157
+ }
158
+ if (typeof globalThis !== "undefined") {
159
+ globalThis.__okstraReportViewExports__ = {
160
+ buildUserResponseMarkdown: buildUserResponseMarkdown,
161
+ };
162
+ }
163
+ })();
@@ -0,0 +1,69 @@
1
+ <!-- single source of truth: scripts/okstra_ctl/report_views.py serialize_user_response -->
2
+ <!-- byte-identical client implementation: templates/reports/report.js buildUserResponseMarkdown -->
3
+
4
+ # User-Response Sidecar Template
5
+
6
+ 이 파일은 final-report HTML 의 **Export user response** 버튼이 만들어내는 markdown 의 표준 포맷을 정의합니다. 산출물은 `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` 경로에 저장됩니다. 원본 final-report MD 는 어떤 경우에도 머지하지 않습니다.
7
+
8
+ ## Frontmatter 스키마
9
+
10
+ ```yaml
11
+ task-key: <task-group>/<task-id>
12
+ task-type: <requirements-discovery | error-analysis | implementation-planning | implementation | final-verification | release-handoff>
13
+ seq: <3-digit zero-padded run sequence>
14
+ source-report: <project-relative path to the final-report .md the HTML was derived from>
15
+ created-by: user
16
+ created-at: <ISO 8601 UTC timestamp>
17
+ ```
18
+
19
+ ## Body 스키마
20
+
21
+ 본문은 `# User Response` 단일 H1 헤딩 아래, 각 응답을 `## <Response ID>` 헤딩으로 구분합니다. Response ID 는 final-report `## 5. Clarification Items` 표의 `ID` 컬럼(`C-NNN`)을 그대로 인용합니다.
22
+
23
+ 각 응답 블록의 schema:
24
+
25
+ ```markdown
26
+ ## <Response ID>
27
+ - Kind: <material | decision | data-point>
28
+ - Value:
29
+ > <multi-line value, trimmed>
30
+ - Rationale: <optional one-line rationale>
31
+ ```
32
+
33
+ 빈 응답 집합(사용자가 어떤 행도 채우지 않고 Export 를 누른 경우)은 다음 한 줄을 본문에 출력합니다:
34
+
35
+ ```markdown
36
+ _(No user responses recorded.)_
37
+ ```
38
+
39
+ ## Example
40
+
41
+ ```markdown
42
+ ---
43
+ task-key: demo/T-1
44
+ task-type: implementation-planning
45
+ seq: 003
46
+ source-report: runs/implementation-planning/reports/final-report-implementation-planning-003.md
47
+ created-by: user
48
+ created-at: 2026-05-17T10:00:00Z
49
+ ---
50
+
51
+ # User Response
52
+
53
+ ## C-001
54
+ - Kind: decision
55
+ - Value:
56
+ > (a) 일회성. 재발 없음.
57
+ - Rationale: 결제 로그 재확인 결과 동일 패턴 미관측.
58
+
59
+ ## C-003
60
+ - Kind: data-point
61
+ - Value:
62
+ > (prediction=0: 1,204) (prediction=1: 38)
63
+ ```
64
+
65
+ ## 호환성 규칙
66
+
67
+ - `Kind` 가 알 수 없는 값이면 폼은 `<textarea>` fallback 으로 렌더되며, 직렬화 시 받은 `Kind` 문자열을 그대로 보존합니다.
68
+ - `Status` 가 `resolved` 또는 `obsolete` 인 행은 HTML 폼이 `disabled` 로 렌더되어 Export 결과에서 자동 제외됩니다. 그 행을 다시 열려면 final-report 의 `Status` 를 `open` / `answered` 로 되돌리고 보고서를 재생성해야 합니다.
69
+ - Python (`scripts/okstra_ctl/report_views.py` `serialize_user_response`) 과 JavaScript (`templates/reports/report.js` `buildUserResponseMarkdown`) 의 출력은 **byte-identical** 이어야 합니다. `tests/test_report_views.py` 가 두 구현의 동일성을 검증합니다.
@@ -250,6 +250,24 @@ for worker in team_state.get("workers", []):
250
250
  )
251
251
  + "\n"
252
252
  )
253
+ # Mirror the audit sidecar contract — every completed worker-results
254
+ # file ships alongside `<worker>-audit-<task-type>-<seq>.md` carrying
255
+ # the Reading Confirmation block. Derive the sidecar path by
256
+ # inserting `-audit` after the worker-role segment of the
257
+ # result-file stem.
258
+ result_stem = result_path.stem # e.g. claude-worker-error-analysis-001
259
+ audit_stem = result_stem.replace("-worker-", "-worker-audit-", 1)
260
+ audit_path = result_path.with_name(f"{audit_stem}{result_path.suffix}")
261
+ audit_path.write_text(
262
+ "\n".join(
263
+ [
264
+ f"# {worker.get('role', worker_id)} Audit",
265
+ "",
266
+ "- Read task-brief.md end-to-end (validation fixture).",
267
+ ]
268
+ )
269
+ + "\n"
270
+ )
253
271
 
254
272
  lead = team_state.get("lead")
255
273
  if isinstance(lead, dict):
@@ -305,6 +323,16 @@ if not isinstance(required_status_entries, list):
305
323
  report_lines = [
306
324
  "# Validation Fixture Report",
307
325
  "",
326
+ "## Verdict Card",
327
+ "",
328
+ "| 항목 | 값 |",
329
+ "|------|----|",
330
+ "| Final Conclusion | validation fixture |",
331
+ "| Verdict Token | `accepted` |",
332
+ "| Direction | `continue-investigation` |",
333
+ "| Approval Required? | `no` |",
334
+ "| Next Step | fixture |",
335
+ "",
308
336
  "## Agent Execution Status",
309
337
  ]
310
338
  for label in required_status_entries:
@@ -313,13 +341,59 @@ for label in required_status_entries:
313
341
  report_lines.extend(
314
342
  [
315
343
  "",
316
- "## Final Verdict",
317
- "- Validation fixture report generated.",
344
+ "## Token Usage Summary",
345
+ "",
346
+ "| 항목 | 처리 토큰 | 환산 토큰 | 비용 (USD) |",
347
+ "|------|-----------|-----------|------------|",
348
+ "| Lead | `1` | `1` | `$0.01` |",
349
+ "| Worker 합계 | `1` | `1` | `$0.01` |",
350
+ "| **전체 합계** | **`2`** | **`2`** | **`$0.02`** |",
351
+ "| Codex/Gemini CLI 추가 비용 | | | `$0.00` |",
352
+ "",
353
+ "## 2. Final Verdict",
354
+ "",
355
+ "| 항목 | 값 |",
356
+ "|------|----|",
357
+ "| Verdict Token | `accepted` |",
358
+ "",
359
+ "## 4.8 Final Verification Deliverables",
360
+ "",
361
+ "Source Implementation Report / Acceptance Blockers / Residual Risk / "
362
+ "Validation Evidence / Read-only Command Log / Routing Recommendation: "
363
+ "fixture stub.",
318
364
  ]
319
365
  )
320
366
  report_path.parent.mkdir(parents=True, exist_ok=True)
321
367
  report_path.write_text("\n".join(report_lines) + "\n")
322
368
 
369
+ # Phase 7 step 1.5 (BLOCKING) — render the slim/html sibling artifacts
370
+ # next to the final-report so validate-run.py's new report-views hook
371
+ # passes. The workflow validator's fixture predates that step; we
372
+ # materialise both files in-place using the same single-reference-point
373
+ # helper the CLI uses.
374
+ import os
375
+ WORKSPACE_ROOT = os.environ.get("OKSTRA_WORKSPACE_ROOT_FOR_FIXTURE", "")
376
+ if WORKSPACE_ROOT:
377
+ import sys as _sys
378
+ _sys.path.insert(0, str(Path(WORKSPACE_ROOT) / "scripts"))
379
+ try:
380
+ from okstra_ctl.report_views import RunMeta, render_both_views
381
+ css = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.css").read_text(encoding="utf-8")
382
+ js = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.js").read_text(encoding="utf-8")
383
+ render_both_views(
384
+ report_path,
385
+ run_meta=RunMeta(
386
+ task_key=str(task_manifest.get("taskKey", "validation/fixture")),
387
+ task_type=str(task_manifest.get("taskType", "validation")),
388
+ seq="001",
389
+ source_report=report_path.name,
390
+ ),
391
+ css=css,
392
+ js=js,
393
+ )
394
+ except Exception as exc: # pragma: no cover — fixture path only
395
+ raise SystemExit(f"failed to render report views in fixture: {exc}")
396
+
323
397
  if final_status_path.exists():
324
398
  final_status_path.unlink()
325
399