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.
- package/bin/okstra +1 -0
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +167 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -5
- package/runtime/agents/workers/codex-worker.md +5 -4
- package/runtime/agents/workers/gemini-worker.md +5 -4
- package/runtime/agents/workers/report-writer-worker.md +10 -3
- package/runtime/bin/okstra-render-report-views.py +129 -0
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +12 -4
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/report_views.py +701 -0
- package/runtime/python/okstra_token_usage/cli.py +9 -2
- package/runtime/python/okstra_token_usage/report.py +32 -3
- package/runtime/skills/okstra-convergence/SKILL.md +2 -2
- package/runtime/skills/okstra-report-writer/SKILL.md +25 -8
- package/runtime/skills/okstra-team-contract/SKILL.md +16 -15
- package/runtime/templates/reports/final-report.template.md +398 -211
- package/runtime/templates/reports/report.css +151 -0
- package/runtime/templates/reports/report.js +163 -0
- package/runtime/templates/reports/user-response.template.md +69 -0
- package/runtime/validators/lib/fixtures.sh +76 -2
- package/runtime/validators/validate-report-views.py +283 -0
- package/runtime/validators/validate-run.py +564 -4
- package/runtime/validators/validate-workflow.sh +4 -0
- package/src/install.mjs +1 -0
- 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
|
-
"##
|
|
317
|
-
"
|
|
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
|
|