okstra 0.72.0 → 0.74.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 -1
- package/docs/kr/cli.md +1 -1
- package/docs/kr/performance-improvement-plan-v2.md +27 -7
- package/docs/project-structure-overview.md +1 -0
- package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +1000 -0
- package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +85 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +3 -3
- package/runtime/agents/workers/codex-worker.md +5 -5
- package/runtime/agents/workers/gemini-worker.md +5 -5
- package/runtime/prompts/profiles/_implementation-executor.md +1 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +11 -2
- package/runtime/python/okstra_ctl/clarification_items.py +32 -22
- package/runtime/python/okstra_ctl/context_cost.py +32 -5
- package/runtime/python/okstra_ctl/md_table.py +56 -0
- package/runtime/python/okstra_ctl/render_final_report.py +8 -1
- package/runtime/python/okstra_ctl/report_views.py +218 -26
- package/runtime/python/okstra_ctl/run.py +4 -4
- package/runtime/python/okstra_ctl/user_response.py +55 -0
- package/runtime/python/okstra_ctl/wizard.py +116 -11
- package/runtime/python/okstra_token_usage/claude.py +22 -0
- package/runtime/python/okstra_token_usage/collect.py +51 -3
- package/runtime/python/okstra_token_usage/cursor.py +3 -1
- package/runtime/schemas/final-report-v1.0.schema.json +2 -1
- package/runtime/skills/okstra-convergence/SKILL.md +8 -4
- package/runtime/skills/okstra-inspect/SKILL.md +20 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +4 -3
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
- package/runtime/templates/reports/final-report.template.md +57 -52
- package/runtime/templates/reports/report.css +21 -7
- package/runtime/templates/reports/report.js +56 -8
- package/runtime/templates/reports/user-response.template.md +15 -0
- package/runtime/validators/validate-implementation-plan-stages.py +9 -2
- package/runtime/validators/validate-report-views.py +2 -1
- package/runtime/validators/validate-run.py +6 -17
- package/runtime/validators/validate-schedule.py +9 -2
- package/runtime/validators/validate_improvement_report.py +2 -1
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
Consumed by scripts/okstra-render-final-report.py against the JSON
|
|
3
3
|
SSOT at runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json.
|
|
4
4
|
Custom filters: format_int, format_usd, format_duration_ms,
|
|
5
|
-
yaml_scalar, yaml_inline_list (defined in
|
|
5
|
+
yaml_scalar, yaml_inline_list, model_detail, mdcell (defined in
|
|
6
|
+
render_final_report.py). Every data interpolation inside a table row
|
|
7
|
+
carries `| mdcell` so a literal `|` in worker prose (e.g. "PASS|FAIL")
|
|
8
|
+
cannot split the row — except t() labels and format_int/format_usd/
|
|
9
|
+
format_duration_ms outputs, which cannot contain `|` by construction.
|
|
10
|
+
Never apply mdcell outside table cells (code fences, headings, prose).
|
|
6
11
|
Token Usage cells render as `--` while tokenUsage.*.totalTokens is
|
|
7
12
|
null in Phase 6; Phase 7 mutates the data.json then re-renders. #}
|
|
8
13
|
---
|
|
@@ -47,7 +52,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
47
52
|
| {{ t("columns.recordMeta") }} | {{ t("columns.summary") }} |
|
|
48
53
|
|--------|------------|
|
|
49
54
|
{% for row in summary -%}
|
|
50
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>{{ t("columns.source") }}: {{ row.source }} | {{ row.summary }} |
|
|
55
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}`<br>{{ t("columns.source") }}: {{ row.source | mdcell }} | {{ row.summary | mdcell }} |
|
|
51
56
|
{% endfor %}
|
|
52
57
|
|
|
53
58
|
## Verdict Card
|
|
@@ -56,11 +61,11 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
56
61
|
|
|
57
62
|
| {{ t("verdictCard.tableHeaderLabel") }} | {{ t("verdictCard.tableHeaderValue") }} |
|
|
58
63
|
|------|----|
|
|
59
|
-
| Final Conclusion | {{ verdictCard.finalConclusion }} |
|
|
60
|
-
| Verdict Token | `{{ verdictCard.verdictToken }}` |
|
|
61
|
-
| Direction | `{{ verdictCard.direction }}` |
|
|
64
|
+
| Final Conclusion | {{ verdictCard.finalConclusion | mdcell }} |
|
|
65
|
+
| Verdict Token | `{{ verdictCard.verdictToken | mdcell }}` |
|
|
66
|
+
| Direction | `{{ verdictCard.direction | mdcell }}` |
|
|
62
67
|
| Approval Required? | `{% if verdictCard.approvalRequired %}yes — {{ t("verdictCard.approvalRequiredSuffix") }}{% else %}no{% endif %}` |
|
|
63
|
-
| Next Step | {{ verdictCard.nextStep }} |
|
|
68
|
+
| Next Step | {{ verdictCard.nextStep | mdcell }} |
|
|
64
69
|
|
|
65
70
|
## 1. Clarification Items
|
|
66
71
|
|
|
@@ -77,7 +82,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
77
82
|
| {{ t("columns.recordMeta") }} | Statement | Expected form | User input |
|
|
78
83
|
|---|---|---|---|
|
|
79
84
|
{% for row in clarificationItems -%}
|
|
80
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Kind: `{{ row.kind }}`<br>Blocks: `{{ row.blocks }}`<br>Status: {{ row.status }} | {{ row.statement }} | {{ row.expectedForm }} | {{ row.userInput or '' }} |
|
|
85
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}`<br>Kind: `{{ row.kind | mdcell }}`<br>Blocks: `{{ row.blocks | mdcell }}`<br>Status: {{ row.status | mdcell }} | {{ row.statement | mdcell }} | {{ row.expectedForm | mdcell }} | {{ (row.userInput or '') | mdcell }} |
|
|
81
86
|
{% endfor %}
|
|
82
87
|
{%- endif %}
|
|
83
88
|
|
|
@@ -91,7 +96,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
91
96
|
| {{ t("columns.recordMeta") }} | Evidence |
|
|
92
97
|
|--------|----------|
|
|
93
98
|
{% for row in evidence.primary -%}
|
|
94
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Source items: {{ row.sourceItems | join(', ') }}<br>Source: {{ row.source }} | {{ row.evidence }} |
|
|
99
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}`<br>Source items: {{ row.sourceItems | join(', ') | mdcell }}<br>Source: {{ row.source | mdcell }} | {{ row.evidence | mdcell }} |
|
|
95
100
|
{% endfor %}
|
|
96
101
|
{%- endif %}
|
|
97
102
|
|
|
@@ -105,7 +110,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
105
110
|
| {{ t("columns.recordMeta") }} | Hypothesis or supporting evidence | Source / confidence |
|
|
106
111
|
|--------|-----------------------------------|---------------------|
|
|
107
112
|
{% for row in evidence.secondary -%}
|
|
108
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}` | {{ row.hypothesis }} | {{ row.confidence }} |
|
|
113
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}` | {{ row.hypothesis | mdcell }} | {{ row.confidence | mdcell }} |
|
|
109
114
|
{% endfor %}
|
|
110
115
|
{%- endif %}
|
|
111
116
|
|
|
@@ -134,7 +139,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
134
139
|
| {{ t("columns.recordMeta") }} | Title | Scope (files/areas) | Reason / Why deferred |
|
|
135
140
|
|--------|-------|---------------------|------------------------|
|
|
136
141
|
{% for row in followUpTasks -%}
|
|
137
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Origin: `{{ row.origin }}`<br>New Task ID: `{{ row.newTaskId }}`<br>Type: `{{ row.suggestedTaskType }}`<br>Priority: `{{ row.priority }}`<br>Auto-spawn: `{{ row.autoSpawn }}` | {{ row.title }} | {{ row.scope }} | {{ row.reason }} |
|
|
142
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}`<br>Origin: `{{ row.origin | mdcell }}`<br>New Task ID: `{{ row.newTaskId | mdcell }}`<br>Type: `{{ row.suggestedTaskType | mdcell }}`<br>Priority: `{{ row.priority | mdcell }}`<br>Auto-spawn: `{{ row.autoSpawn | mdcell }}` | {{ row.title | mdcell }} | {{ row.scope | mdcell }} | {{ row.reason | mdcell }} |
|
|
138
143
|
{% endfor %}
|
|
139
144
|
{%- endif %}
|
|
140
145
|
|
|
@@ -146,7 +151,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
146
151
|
| {{ t("columns.recordMeta") }} | Item | Risk if ignored | Mitigation Owner |
|
|
147
152
|
|--------|------|-----------------|------------------|
|
|
148
153
|
{% for row in missingInformation -%}
|
|
149
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}` | {{ row.item }} | {{ row.risk }} | {{ row.owner }} |
|
|
154
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}` | {{ row.item | mdcell }} | {{ row.risk | mdcell }} | {{ row.owner | mdcell }} |
|
|
150
155
|
{% endfor %}
|
|
151
156
|
{%- endif %}
|
|
152
157
|
|
|
@@ -163,7 +168,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
163
168
|
| ID | Ticket ID | Action | Path (and line-range) | Change summary |
|
|
164
169
|
|----|-----------|--------|------------------------|----------------|
|
|
165
170
|
{% for fs in opt.fileStructure -%}
|
|
166
|
-
| {{ fs.id }} | `{{ fs.ticketId }}` | {{ fs.action }} | `{{ fs.path }}` | {{ fs.summary }} |
|
|
171
|
+
| {{ fs.id | mdcell }} | `{{ fs.ticketId | mdcell }}` | {{ fs.action | mdcell }} | `{{ fs.path | mdcell }}` | {{ fs.summary | mdcell }} |
|
|
167
172
|
{% endfor %}
|
|
168
173
|
|
|
169
174
|
- {{ t("implementationPlanning.optionInterfacesLabel") }}: {{ opt.interfaces }}
|
|
@@ -176,17 +181,17 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
176
181
|
| Option | Complexity | Risk | Reversibility | Test Coverage Cost | Rollout Cost |
|
|
177
182
|
|--------|-----------|------|---------------|--------------------|--------------|
|
|
178
183
|
{% for row in implementationPlanning.tradeoffMatrix -%}
|
|
179
|
-
| {{ row.option }} | {{ row.complexity }} | {{ row.risk }} | {{ row.reversibility }} | {{ row.testCoverageCost }} | {{ row.rolloutCost }} |
|
|
184
|
+
| {{ row.option | mdcell }} | {{ row.complexity | mdcell }} | {{ row.risk | mdcell }} | {{ row.reversibility | mdcell }} | {{ row.testCoverageCost | mdcell }} | {{ row.rolloutCost | mdcell }} |
|
|
180
185
|
{% endfor %}
|
|
181
186
|
|
|
182
187
|
### Recommended Option{% if t("sectionAside.recommendedOption") != "Recommended Option" %} ({{ t("sectionAside.recommendedOption") }}){% endif %}
|
|
183
188
|
|
|
184
189
|
| {{ t("implementationPlanning.recommendedTableHeaderLabel") }} | {{ t("implementationPlanning.recommendedTableHeaderValue") }} |
|
|
185
190
|
|------|----|
|
|
186
|
-
| Recommended Option | {{ implementationPlanning.recommendedOption.name }} |
|
|
187
|
-
| {{ t("implementationPlanning.coreReasonLabel") }} | {{ implementationPlanning.recommendedOption.coreReason }} |
|
|
188
|
-
| {{ t("implementationPlanning.rationaleLabel") }} | {{ implementationPlanning.recommendedOption.rationale }} |
|
|
189
|
-
| {{ t("implementationPlanning.rejectedSummaryLabel") }} | {{ implementationPlanning.recommendedOption.rejectedSummary }} |
|
|
191
|
+
| Recommended Option | {{ implementationPlanning.recommendedOption.name | mdcell }} |
|
|
192
|
+
| {{ t("implementationPlanning.coreReasonLabel") }} | {{ implementationPlanning.recommendedOption.coreReason | mdcell }} |
|
|
193
|
+
| {{ t("implementationPlanning.rationaleLabel") }} | {{ implementationPlanning.recommendedOption.rationale | mdcell }} |
|
|
194
|
+
| {{ t("implementationPlanning.rejectedSummaryLabel") }} | {{ implementationPlanning.recommendedOption.rejectedSummary | mdcell }} |
|
|
190
195
|
|
|
191
196
|
### Dependency / Migration Risk{% if t("sectionAside.dependencyRisk") != "Dependency / Migration Risk" %} ({{ t("sectionAside.dependencyRisk") }}){% endif %}
|
|
192
197
|
|
|
@@ -196,16 +201,16 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
196
201
|
| ID | Kind | Item | {{ t("implementationPlanning.columnImpact") }} | {{ t("implementationPlanning.columnMitigation") }} |
|
|
197
202
|
|----|------|------|------|------------------|
|
|
198
203
|
{% for row in implementationPlanning.dependencyMigrationRisk -%}
|
|
199
|
-
| {{ row.id }} | {{ row.kind }} | {{ row.item }} | {{ row.impact }} | {{ row.mitigation }} |
|
|
204
|
+
| {{ row.id | mdcell }} | {{ row.kind | mdcell }} | {{ row.item | mdcell }} | {{ row.impact | mdcell }} | {{ row.mitigation | mdcell }} |
|
|
200
205
|
{% endfor %}
|
|
201
206
|
{%- endif %}
|
|
202
207
|
|
|
203
208
|
### Validation Checklist{% if t("sectionAside.validationChecklist") != "Validation Checklist" %} ({{ t("sectionAside.validationChecklist") }}){% endif %}
|
|
204
209
|
|
|
205
|
-
| Phase | Ticket ID | Check | Command / Observation | Expected outcome |
|
|
206
|
-
|
|
210
|
+
| ID | Phase | Ticket ID | Check | Command / Observation | Expected outcome |
|
|
211
|
+
|----|-------|-----------|-------|------------------------|-------------------|
|
|
207
212
|
{% for row in implementationPlanning.validationChecklist -%}
|
|
208
|
-
| {{ row.phase }} | `{{ row.ticketId }}` | {{ row.check }} | `{{ row.commandOrObservation }}` | {{ row.expectedOutcome }} |
|
|
213
|
+
| {{ row.id | mdcell }} | {{ row.phase | mdcell }} | `{{ row.ticketId | mdcell }}` | {{ row.check | mdcell }} | `{{ row.commandOrObservation | mdcell }}` | {{ row.expectedOutcome | mdcell }} |
|
|
209
214
|
{% endfor %}
|
|
210
215
|
|
|
211
216
|
### Rollback Strategy{% if t("sectionAside.rollbackStrategy") != "Rollback Strategy" %} ({{ t("sectionAside.rollbackStrategy") }}){% endif %}
|
|
@@ -213,7 +218,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
213
218
|
| ID | Step | Action | Trigger signal | {{ t("columns.checkMethod") }} |
|
|
214
219
|
|----|------|--------|----------------|-----------|
|
|
215
220
|
{% for row in implementationPlanning.rollbackStrategy -%}
|
|
216
|
-
| {{ row.id }} | {{ row.step }} | `{{ row.action }}` | {{ row.triggerSignal }} | `{{ row.verificationMethod }}` |
|
|
221
|
+
| {{ row.id | mdcell }} | {{ row.step | mdcell }} | `{{ row.action | mdcell }}` | {{ row.triggerSignal | mdcell }} | `{{ row.verificationMethod | mdcell }}` |
|
|
217
222
|
{% endfor %}
|
|
218
223
|
|
|
219
224
|
### Requirement Coverage
|
|
@@ -221,7 +226,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
221
226
|
| ID | Source | Requirement | Covered by option / stage / step | Status |
|
|
222
227
|
|----|--------|-------------|-----------------------------------|--------|
|
|
223
228
|
{% for row in implementationPlanning.requirementCoverage -%}
|
|
224
|
-
| {{ row.id }} | `{{ row.source }}` | {{ row.requirement }} | {{ row.coveredBy }} | `{{ row.status }}` |
|
|
229
|
+
| {{ row.id | mdcell }} | `{{ row.source | mdcell }}` | {{ row.requirement | mdcell }} | {{ row.coveredBy | mdcell }} | `{{ row.status | mdcell }}` |
|
|
225
230
|
{% endfor %}
|
|
226
231
|
|
|
227
232
|
## 5.5 Stage Map
|
|
@@ -229,7 +234,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
229
234
|
| stage | title | depends-on | step-count | exit-contract-summary |
|
|
230
235
|
|-------|-------|------------|------------|-----------------------|
|
|
231
236
|
{% for row in implementationPlanning.stageMap -%}
|
|
232
|
-
| {{ row.stage }} | {{ row.title }} | {{ row.dependsOn }} | {{ row.stepCount }} | {{ row.exitContractSummary }} |
|
|
237
|
+
| {{ row.stage | mdcell }} | {{ row.title | mdcell }} | {{ row.dependsOn | mdcell }} | {{ row.stepCount | mdcell }} | {{ row.exitContractSummary | mdcell }} |
|
|
233
238
|
{% endfor %}
|
|
234
239
|
|
|
235
240
|
{% for stage in implementationPlanning.stages %}
|
|
@@ -255,7 +260,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
255
260
|
| step | action | files | command | expected |
|
|
256
261
|
|------|--------|-------|---------|----------|
|
|
257
262
|
{% for row in stage.stepwiseExecution -%}
|
|
258
|
-
| {{ row.step }} | {{ row.action }} | `{{ row.files }}` | `{{ row.command }}` | {{ row.expected }} |
|
|
263
|
+
| {{ row.step | mdcell }} | {{ row.action | mdcell }} | `{{ row.files | mdcell }}` | `{{ row.command | mdcell }}` | {{ row.expected | mdcell }} |
|
|
259
264
|
{% endfor %}
|
|
260
265
|
|
|
261
266
|
### Stage Exit Contract
|
|
@@ -280,7 +285,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
280
285
|
| Plan item | Worker | Verdict | Breakage kind | Note |
|
|
281
286
|
|-----------|--------|---------|---------------|------|
|
|
282
287
|
{% for row in implementationPlanning.planBodyVerification.verdictDetails -%}
|
|
283
|
-
| {{ row.planItem }} | {{ row.worker }} | {{ row.verdict }} | {{ row.breakageKind or '--' }} | {{ row.note or '' }} |
|
|
288
|
+
| {{ row.planItem | mdcell }} | {{ row.worker | mdcell }} | {{ row.verdict | mdcell }} | {{ (row.breakageKind or '--') | mdcell }} | {{ (row.note or '') | mdcell }} |
|
|
284
289
|
{% endfor %}
|
|
285
290
|
|
|
286
291
|
#### Dissent log
|
|
@@ -318,10 +323,10 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
318
323
|
|
|
319
324
|
| {{ t("releaseHandoff.questionsTableHeader.questionId") }} | {{ t("releaseHandoff.questionsTableHeader.questionBody") }} | {{ t("releaseHandoff.questionsTableHeader.userResponse") }} | {{ t("releaseHandoff.questionsTableHeader.allowedOptions") }} |
|
|
320
325
|
|---------|-----------|--------------------|--------------------|
|
|
321
|
-
| H1 | {{ t("releaseHandoff.h1Body") }} | `{{ releaseHandoff.userSelections.h1 }}` | `local only` / `push + PR` / `skip` |
|
|
322
|
-
| H2 | {{ t("releaseHandoff.h2Body") }} | `{{ releaseHandoff.userSelections.h2 or t("releaseHandoff.h2DefaultLabel") }}` | {{ t("releaseHandoff.h2OptionsLabel") }} |
|
|
323
|
-
| H2b | Merge conflict probe | `{{ releaseHandoff.userSelections.h2b or releaseHandoff.mergeConflictProbe.kind }}` | `not-run` / `clean` / `proceed anyway` / `change base branch` / `cancel` |
|
|
324
|
-
| H3 | {{ t("releaseHandoff.h3Body") }} | `{{ releaseHandoff.userSelections.h3 }}` | `use as-is` / `edit then proceed` / `cancel` |
|
|
326
|
+
| H1 | {{ t("releaseHandoff.h1Body") }} | `{{ releaseHandoff.userSelections.h1 | mdcell }}` | `local only` / `push + PR` / `skip` |
|
|
327
|
+
| H2 | {{ t("releaseHandoff.h2Body") }} | `{{ (releaseHandoff.userSelections.h2 or t("releaseHandoff.h2DefaultLabel")) | mdcell }}` | {{ t("releaseHandoff.h2OptionsLabel") }} |
|
|
328
|
+
| H2b | Merge conflict probe | `{{ (releaseHandoff.userSelections.h2b or releaseHandoff.mergeConflictProbe.kind) | mdcell }}` | `not-run` / `clean` / `proceed anyway` / `change base branch` / `cancel` |
|
|
329
|
+
| H3 | {{ t("releaseHandoff.h3Body") }} | `{{ releaseHandoff.userSelections.h3 | mdcell }}` | `use as-is` / `edit then proceed` / `cancel` |
|
|
325
330
|
|
|
326
331
|
### 5.6.4 Executed Commands
|
|
327
332
|
|
|
@@ -331,7 +336,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
331
336
|
| # | command (verbatim) | exit code | {{ t("releaseHandoff.commandsTableHeader.outputSummary") }} |
|
|
332
337
|
|---|---------------------|-----------|--------------------|
|
|
333
338
|
{% for row in releaseHandoff.executedCommands -%}
|
|
334
|
-
| {{ row.number }} | `{{ row.command }}` | `{{ row.exitCode }}` | {{ row.outputSummary }} |
|
|
339
|
+
| {{ row.number | mdcell }} | `{{ row.command | mdcell }}` | `{{ row.exitCode | mdcell }}` | {{ row.outputSummary | mdcell }} |
|
|
335
340
|
{% endfor %}
|
|
336
341
|
{%- endif %}
|
|
337
342
|
|
|
@@ -343,7 +348,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
343
348
|
| Short SHA | Full SHA | Subject | Files |
|
|
344
349
|
|-----------|----------|---------|-------|
|
|
345
350
|
{% for row in releaseHandoff.commitList -%}
|
|
346
|
-
| `{{ row.shortSha }}` | `{{ row.fullSha }}` | {{ row.subject }} | {{ row.files }} |
|
|
351
|
+
| `{{ row.shortSha | mdcell }}` | `{{ row.fullSha | mdcell }}` | {{ row.subject | mdcell }} | {{ row.files | mdcell }} |
|
|
347
352
|
{% endfor %}
|
|
348
353
|
{%- endif %}
|
|
349
354
|
|
|
@@ -393,7 +398,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
393
398
|
| # | Short SHA | Full SHA | Plan Step | Subject | Files |
|
|
394
399
|
|---|-----------|----------|-----------|---------|-------|
|
|
395
400
|
{% for row in implementation.commitList -%}
|
|
396
|
-
| {{ row.number }} | `{{ row.shortSha }}` | `{{ row.fullSha }}` | {{ row.planStep }} | {{ row.subject }} | {{ row.files }} |
|
|
401
|
+
| {{ row.number | mdcell }} | `{{ row.shortSha | mdcell }}` | `{{ row.fullSha | mdcell }}` | {{ row.planStep | mdcell }} | {{ row.subject | mdcell }} | {{ row.files | mdcell }} |
|
|
397
402
|
{% endfor %}
|
|
398
403
|
{%- endif %}
|
|
399
404
|
|
|
@@ -406,7 +411,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
406
411
|
| File | Action | Lines (+/-) | Plan step / Out-of-plan |
|
|
407
412
|
|------|--------|-------------|--------------------------|
|
|
408
413
|
{% for row in implementation.diffSummary.files -%}
|
|
409
|
-
| `{{ row.file }}` | {{ row.action }} | `{{ row.lines }}` | {{ row.planStep }} |
|
|
414
|
+
| `{{ row.file | mdcell }}` | {{ row.action | mdcell }} | `{{ row.lines | mdcell }}` | {{ row.planStep | mdcell }} |
|
|
410
415
|
{% endfor %}
|
|
411
416
|
|
|
412
417
|
### 5.7.4 Out-of-plan Edits
|
|
@@ -417,7 +422,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
417
422
|
| ID | File | Rationale | Trigger |
|
|
418
423
|
|----|------|-----------|---------|
|
|
419
424
|
{% for row in implementation.outOfPlanEdits -%}
|
|
420
|
-
| {{ row.id }} | `{{ row.file }}` | {{ row.rationale }} | {{ row.trigger }} |
|
|
425
|
+
| {{ row.id | mdcell }} | `{{ row.file | mdcell }}` | {{ row.rationale | mdcell }} | {{ row.trigger | mdcell }} |
|
|
421
426
|
{% endfor %}
|
|
422
427
|
{%- endif %}
|
|
423
428
|
|
|
@@ -438,7 +443,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
438
443
|
| Phase | Command | Exit code | Output tail | TDD evidence |
|
|
439
444
|
|-------|---------|-----------|-------------|--------------|
|
|
440
445
|
{% for row in implementation.validationEvidence -%}
|
|
441
|
-
| {{ row.phase }} | `{{ row.command }}` | `{{ row.exitCode }}` | {{ row.outputTail }} | {{ row.tddEvidence or '--' }} |
|
|
446
|
+
| {{ row.phase | mdcell }} | `{{ row.command | mdcell }}` | `{{ row.exitCode | mdcell }}` | {{ row.outputTail | mdcell }} | {{ (row.tddEvidence or '--') | mdcell }} |
|
|
442
447
|
{% endfor %}
|
|
443
448
|
|
|
444
449
|
### 5.7.7 Verifier Results
|
|
@@ -457,7 +462,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
457
462
|
| Category | Rollback command | Verification | Result |
|
|
458
463
|
|----------|-------------------|---------------|--------|
|
|
459
464
|
{% for row in implementation.rollbackVerification -%}
|
|
460
|
-
| {{ row.category }} | `{{ row.rollbackCommand }}` | {{ row.verification }} | `{{ row.result }}` |
|
|
465
|
+
| {{ row.category | mdcell }} | `{{ row.rollbackCommand | mdcell }}` | {{ row.verification | mdcell }} | `{{ row.result | mdcell }}` |
|
|
461
466
|
{% endfor %}
|
|
462
467
|
|
|
463
468
|
### 5.7.9 Routing Recommendation
|
|
@@ -499,7 +504,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
499
504
|
| ID | Severity | Statement | Evidence | Recommended follow-up phase |
|
|
500
505
|
|----|----------|-----------|----------|------------------------------|
|
|
501
506
|
{% for row in finalVerification.acceptanceBlockers -%}
|
|
502
|
-
| {{ row.id }} | `{{ row.severity }}` | {{ row.statement }} | {{ row.evidence }} | `{{ row.followUpPhase }}` |
|
|
507
|
+
| {{ row.id | mdcell }} | `{{ row.severity | mdcell }}` | {{ row.statement | mdcell }} | {{ row.evidence | mdcell }} | `{{ row.followUpPhase | mdcell }}` |
|
|
503
508
|
{% endfor %}
|
|
504
509
|
{%- endif %}
|
|
505
510
|
|
|
@@ -511,7 +516,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
511
516
|
| ID | Item | Mitigation owner | Escalation trigger |
|
|
512
517
|
|----|------|------------------|---------------------|
|
|
513
518
|
{% for row in finalVerification.residualRisk -%}
|
|
514
|
-
| {{ row.id }} | {{ row.item }} | {{ row.owner }} | {{ row.escalationTrigger }} |
|
|
519
|
+
| {{ row.id | mdcell }} | {{ row.item | mdcell }} | {{ row.owner | mdcell }} | {{ row.escalationTrigger | mdcell }} |
|
|
515
520
|
{% endfor %}
|
|
516
521
|
{%- endif %}
|
|
517
522
|
|
|
@@ -520,7 +525,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
520
525
|
| ID | {{ t("finalVerification.columnRequirement") }} | Artifact | Status |
|
|
521
526
|
|----|--------------------------------|----------|--------|
|
|
522
527
|
{% for row in finalVerification.validationEvidence -%}
|
|
523
|
-
| {{ row.id }} | {{ row.requirement }} | `{{ row.artifact }}` | {{ row.status }} |
|
|
528
|
+
| {{ row.id | mdcell }} | {{ row.requirement | mdcell }} | `{{ row.artifact | mdcell }}` | {{ row.status | mdcell }} |
|
|
524
529
|
{% endfor %}
|
|
525
530
|
|
|
526
531
|
### 5.8.5 Read-only Command Log
|
|
@@ -528,7 +533,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
528
533
|
| # | Tier | Command (verbatim) | Status | Exit code | Output tail |
|
|
529
534
|
|---|------|---------------------|--------|-----------|-------------|
|
|
530
535
|
{% for row in finalVerification.readonlyCommandLog -%}
|
|
531
|
-
| {{ row.number }} | {{ row.tier }} | `{{ row.command }}` | `{{ row.status }}` | {{ row.exitCode if row.exitCode is not none else '—' }} | {{ row.rejectionReason if row.status == 'rejected' else row.outputTail }} |
|
|
536
|
+
| {{ row.number | mdcell }} | {{ row.tier | mdcell }} | `{{ row.command | mdcell }}` | `{{ row.status | mdcell }}` | {{ (row.exitCode if row.exitCode is not none else '—') | mdcell }} | {{ (row.rejectionReason if row.status == 'rejected' else row.outputTail) | mdcell }} |
|
|
532
537
|
{% endfor %}
|
|
533
538
|
|
|
534
539
|
### 5.8.6 Conditional Acceptance Conditions
|
|
@@ -539,7 +544,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
539
544
|
| ID | Condition | Evidence required | Blocks release-handoff |
|
|
540
545
|
|----|-----------|-------------------|------------------------|
|
|
541
546
|
{% for row in finalVerdict.conditionalAcceptanceConditions -%}
|
|
542
|
-
| {{ row.id }} | {{ row.condition }} | {{ row.evidenceRequired }} | {{ row.blocksReleaseHandoff }} |
|
|
547
|
+
| {{ row.id | mdcell }} | {{ row.condition | mdcell }} | {{ row.evidenceRequired | mdcell }} | {{ row.blocksReleaseHandoff | mdcell }} |
|
|
543
548
|
{% endfor %}
|
|
544
549
|
{%- endif %}
|
|
545
550
|
|
|
@@ -580,7 +585,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
580
585
|
| Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches (worker:status:durationMs) | skippedWorkers (worker:reason) |
|
|
581
586
|
|-------|----------------|---------------|----------------------|----------------------------------------|---------------------------------|
|
|
582
587
|
{% for row in crossVerification.roundHistory.rounds -%}
|
|
583
|
-
| {{ row.round }} | {{ row.inputQueueSize }} | {{ row.resolvedCount }} | {{ row.carriedForwardCount }} | {{ row.dispatches }} | {{ row.skippedWorkers }} |
|
|
588
|
+
| {{ row.round | mdcell }} | {{ row.inputQueueSize | mdcell }} | {{ row.resolvedCount | mdcell }} | {{ row.carriedForwardCount | mdcell }} | {{ row.dispatches | mdcell }} | {{ row.skippedWorkers | mdcell }} |
|
|
584
589
|
{% endfor %}
|
|
585
590
|
|
|
586
591
|
- `round2SkippedReason`: `{{ crossVerification.roundHistory.round2SkippedReason }}` ← {{ t("roundHistory.round2SkippedReasonNote") }}
|
|
@@ -600,7 +605,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
600
605
|
| {{ t("columns.recordMeta") }} | Statement | Evidence (path:line / log / worker report) |
|
|
601
606
|
|--------|-----------|---------------------------------------------|
|
|
602
607
|
{% for row in crossVerification.consensus -%}
|
|
603
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Source items: {{ row.sourceItems | join(', ') }} | {{ row.statement }} | {{ row.evidence }} |
|
|
608
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}`<br>Source items: {{ row.sourceItems | join(', ') | mdcell }} | {{ row.statement | mdcell }} | {{ row.evidence | mdcell }} |
|
|
604
609
|
{% endfor %}
|
|
605
610
|
{%- endif %}
|
|
606
611
|
|
|
@@ -614,7 +619,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
614
619
|
| {{ t("columns.recordMeta") }} | Disagreement | Evidence |
|
|
615
620
|
|--------|--------------|----------|
|
|
616
621
|
{% for row in crossVerification.differences -%}
|
|
617
|
-
| **{{ row.id }}**<br>Ticket: `{{ row.ticketId }}`<br>Workers: {% for w in row.workersPosition %}{{ w.worker }}:{{ w.itemId }} ({{ w.position }}){% if not loop.last %} / {% endif %}{% endfor %} | {{ row.disagreement }} | {{ row.evidence }} |
|
|
622
|
+
| **{{ row.id | mdcell }}**<br>Ticket: `{{ row.ticketId | mdcell }}`<br>Workers: {% for w in row.workersPosition %}{{ w.worker | mdcell }}:{{ w.itemId | mdcell }} ({{ w.position | mdcell }}){% if not loop.last %} / {% endif %}{% endfor %} | {{ row.disagreement | mdcell }} | {{ row.evidence | mdcell }} |
|
|
618
623
|
{% endfor %}
|
|
619
624
|
{%- endif %}
|
|
620
625
|
|
|
@@ -624,11 +629,11 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
624
629
|
|
|
625
630
|
| {{ t("verdictCard.tableHeaderLabel") }} | {{ t("verdictCard.tableHeaderValue") }} |
|
|
626
631
|
|------|----|
|
|
627
|
-
| Final Conclusion | {{ finalVerdict.finalConclusion }} |
|
|
628
|
-
| Verdict Token | `{{ finalVerdict.verdictToken }}` |
|
|
629
|
-
| Direction | `{{ finalVerdict.direction }}` |
|
|
630
|
-
| {{ t("verdictCard.rationaleLabel") }} | {{ finalVerdict.rationaleRowIds | join(', ') }} |
|
|
631
|
-
| {{ t("verdictCard.nextStepLabel") }} | {{ finalVerdict.nextStep }} |
|
|
632
|
+
| Final Conclusion | {{ finalVerdict.finalConclusion | mdcell }} |
|
|
633
|
+
| Verdict Token | `{{ finalVerdict.verdictToken | mdcell }}` |
|
|
634
|
+
| Direction | `{{ finalVerdict.direction | mdcell }}` |
|
|
635
|
+
| {{ t("verdictCard.rationaleLabel") }} | {{ finalVerdict.rationaleRowIds | join(', ') | mdcell }} |
|
|
636
|
+
| {{ t("verdictCard.nextStepLabel") }} | {{ finalVerdict.nextStep | mdcell }} |
|
|
632
637
|
|
|
633
638
|
{% if ticketCoverage.omit %}
|
|
634
639
|
{# Ticket Coverage omitted entirely — release-handoff / final-verification #}
|
|
@@ -643,7 +648,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
643
648
|
| Ticket ID | {{ t("ticketCoverage.columnSections") }} | {{ t("ticketCoverage.columnRelatedIds") }} |
|
|
644
649
|
|-----------|-----------|---------------|
|
|
645
650
|
{% for row in ticketCoverage.rows -%}
|
|
646
|
-
| `{{ row.ticketId }}` | `{{ row.sections }}` | `{{ row.relatedIds }}` |
|
|
651
|
+
| `{{ row.ticketId | mdcell }}` | `{{ row.sections | mdcell }}` | `{{ row.relatedIds | mdcell }}` |
|
|
647
652
|
{% endfor %}
|
|
648
653
|
{%- endif %}
|
|
649
654
|
|
|
@@ -655,7 +660,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
655
660
|
| {{ t("columns.recordMeta") }} | Summary of Key Findings |
|
|
656
661
|
|--------|-------------------------|
|
|
657
662
|
{% for row in executionStatus -%}
|
|
658
|
-
| **{{ row.agent }}**<br>Role: {{ row.role }}<br>Model: {{ row.model }}<br>Status: {{ row.status }}<br>{{ t("columns.rawTokens") }}: {{ row.totalTokens | format_int }}{% if row.cliTotalTokens %} (CLI: {{ row.cliTotalTokens | format_int }}){% endif %}<br>{{ t("columns.billableTokens") }}: {{ row.billableTokens | format_int }}<br>{{ t("columns.cost") }}: {{ row.costUsd | format_usd }}{% if row.cliCostUsd %} (+ CLI {{ row.cliCostUsd | format_usd }}){% endif %}<br>Duration: {{ row.durationMs | format_duration_ms }} | {{ row.summary }} |
|
|
663
|
+
| **{{ row.agent | mdcell }}**<br>Role: {{ row.role | mdcell }}<br>Model: {{ row.model | mdcell }}<br>Status: {{ row.status | mdcell }}<br>{{ t("columns.rawTokens") }}: {{ row.totalTokens | format_int }}{% if row.cliTotalTokens %} (CLI: {{ row.cliTotalTokens | format_int }}){% endif %}<br>{{ t("columns.billableTokens") }}: {{ row.billableTokens | format_int }}<br>{{ t("columns.cost") }}: {{ row.costUsd | format_usd }}{% if row.cliCostUsd %} (+ CLI {{ row.cliCostUsd | format_usd }}){% endif %}<br>Duration: {{ row.durationMs | format_duration_ms }} | {{ row.summary | mdcell }} |
|
|
659
664
|
{% endfor %}
|
|
660
665
|
|
|
661
666
|
## {{ t("tokenSummary.heading") }}
|
|
@@ -121,13 +121,14 @@ th, td {
|
|
|
121
121
|
/* No max-width: cells that carry prose (statement / source / scope)
|
|
122
122
|
* absorb the remaining horizontal space within `main`'s 120ch. */
|
|
123
123
|
}
|
|
124
|
-
/* Narrow columns —
|
|
125
|
-
* within 30 plain chars
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
124
|
+
/* Narrow columns — assigned by `_narrow_columns()` in report_views.py:
|
|
125
|
+
* either every cell fits within 30 plain chars, or the header is
|
|
126
|
+
* whitelisted (ID, Ticket ID, Kind, Status, 항목, 출처, 에이전트 …)
|
|
127
|
+
* AND no cell carries a <br>-line over 60 plain chars — `nowrap` below
|
|
128
|
+
* makes the longest line the column's intrinsic width, so longer lines
|
|
129
|
+
* must fall back to a plain wrapping column. Pinning `width: 5%` makes
|
|
130
|
+
* narrow columns shrink toward the suggestion while wide neighbours
|
|
131
|
+
* absorb the remainder. */
|
|
131
132
|
td.td-narrow, th.td-narrow {
|
|
132
133
|
width: 5%;
|
|
133
134
|
white-space: nowrap;
|
|
@@ -196,9 +197,22 @@ button[data-action]:hover { background: color-mix(in srgb, Highlight 20%, Button
|
|
|
196
197
|
}
|
|
197
198
|
#user-response-output:empty { display: none; }
|
|
198
199
|
|
|
200
|
+
#plan-approval {
|
|
201
|
+
max-width: 120ch;
|
|
202
|
+
margin: 1.5rem auto;
|
|
203
|
+
padding: 0.8em 1rem;
|
|
204
|
+
border: 1px solid color-mix(in srgb, GrayText 40%, transparent);
|
|
205
|
+
border-radius: 4px;
|
|
206
|
+
}
|
|
207
|
+
#plan-approval h2 { margin-top: 0; }
|
|
208
|
+
#plan-approval label { display: block; margin: 0.4em 0; }
|
|
209
|
+
#plan-approval select { font: inherit; max-width: 100%; }
|
|
210
|
+
.approval-disabled-reason { color: GrayText; margin: 0.4em 0 0; }
|
|
211
|
+
|
|
199
212
|
@media print {
|
|
200
213
|
.report-header, .report-footer { position: static; }
|
|
201
214
|
thead th { position: static; }
|
|
202
215
|
button[data-action] { display: none; }
|
|
203
216
|
#user-response-output { display: none; }
|
|
217
|
+
#plan-approval { display: none; }
|
|
204
218
|
}
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* (everything else / fallback).
|
|
10
10
|
* 2. Serialise the entries into markdown whose bytes are IDENTICAL
|
|
11
11
|
* to scripts/okstra_ctl/report_views.py serialize_user_response.
|
|
12
|
-
* 3. Write the result to <pre id="user-response-output"
|
|
13
|
-
* [Copy] button
|
|
12
|
+
* 3. Write the result to <pre id="user-response-output">, offer a
|
|
13
|
+
* [Copy] button, and download it as
|
|
14
|
+
* user-response-<task-type>-<seq>.md — the user drops that file in
|
|
15
|
+
* runs/<task-type>/user-responses/ and --resume-clarification
|
|
16
|
+
* auto-appends it to the next run's clarification-response.md.
|
|
14
17
|
*
|
|
15
18
|
* The byte-identity contract is enforced by tests/test_report_views.py
|
|
16
19
|
* which spawns Node to execute buildUserResponseMarkdown and diffs the
|
|
@@ -91,6 +94,23 @@
|
|
|
91
94
|
return entries;
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
// Plan Approval 위젯 (implementation-planning 보고서에만 존재). 체크박스가
|
|
98
|
+
// disabled(§1 차단 미해소)이거나 미체크면 null. 옵션이 추천(data-recommended)
|
|
99
|
+
// 그대로면 implementationOption 을 비워 보낸다 → 소비 측 Recommended 폴백.
|
|
100
|
+
function collectApproval() {
|
|
101
|
+
var box = document.getElementById("approval-checkbox");
|
|
102
|
+
if (!box || box.disabled || !box.checked) return null;
|
|
103
|
+
var sel = document.getElementById("approval-option");
|
|
104
|
+
var option = "";
|
|
105
|
+
if (sel && sel.selectedIndex >= 0) {
|
|
106
|
+
var opt = sel.options[sel.selectedIndex];
|
|
107
|
+
if (opt && opt.getAttribute("data-recommended") !== "true") {
|
|
108
|
+
option = opt.value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { approved: true, implementationOption: option };
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
// Toggle the visibility of the "기타" companion input next to each
|
|
95
115
|
// select whose current value is "__other__". Wired at bind() time and
|
|
96
116
|
// also called once for the initial state.
|
|
@@ -110,7 +130,7 @@
|
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
|
|
113
|
-
function buildUserResponseMarkdown(runMeta, entries, createdAt) {
|
|
133
|
+
function buildUserResponseMarkdown(runMeta, entries, createdAt, approval) {
|
|
114
134
|
var head =
|
|
115
135
|
"---\n" +
|
|
116
136
|
"task-key: " + (runMeta["task-key"] || "") + "\n" +
|
|
@@ -123,10 +143,8 @@
|
|
|
123
143
|
"\n" +
|
|
124
144
|
"# User Response\n";
|
|
125
145
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
146
|
+
entries = entries || [];
|
|
147
|
+
var hasApproval = !!(approval && approval.approved);
|
|
130
148
|
var chunks = "";
|
|
131
149
|
for (var i = 0; i < entries.length; i++) {
|
|
132
150
|
var e = entries[i];
|
|
@@ -140,15 +158,44 @@
|
|
|
140
158
|
}
|
|
141
159
|
chunks += chunk;
|
|
142
160
|
}
|
|
161
|
+
if (entries.length === 0 && !hasApproval) {
|
|
162
|
+
chunks += "\n_(No user responses recorded.)_\n";
|
|
163
|
+
}
|
|
164
|
+
if (hasApproval) {
|
|
165
|
+
chunks += "\n## APPROVAL\n- Approved: true\n";
|
|
166
|
+
if (approval.implementationOption) {
|
|
167
|
+
chunks +=
|
|
168
|
+
"- Implementation-Option: " + trimMultiline(approval.implementationOption) + "\n";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
143
171
|
return head + chunks;
|
|
144
172
|
}
|
|
145
173
|
|
|
174
|
+
function downloadUserResponse(md, runMeta) {
|
|
175
|
+
var name =
|
|
176
|
+
"user-response-" + (runMeta["task-type"] || "report") + "-" +
|
|
177
|
+
(runMeta["seq"] || "000") + ".md";
|
|
178
|
+
var blob = new Blob([md], { type: "text/markdown" });
|
|
179
|
+
var url = URL.createObjectURL(blob);
|
|
180
|
+
var a = document.createElement("a");
|
|
181
|
+
a.href = url;
|
|
182
|
+
a.download = name;
|
|
183
|
+
document.body.appendChild(a);
|
|
184
|
+
a.click();
|
|
185
|
+
document.body.removeChild(a);
|
|
186
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
|
|
187
|
+
}
|
|
188
|
+
|
|
146
189
|
function exportUserResponse() {
|
|
147
190
|
var runMeta = readRunMeta();
|
|
148
191
|
var entries = collectEntries();
|
|
149
|
-
var
|
|
192
|
+
var approval = collectApproval();
|
|
193
|
+
var md = buildUserResponseMarkdown(runMeta, entries, isoNowUtc(), approval);
|
|
150
194
|
var out = document.getElementById("user-response-output");
|
|
151
195
|
if (out) out.textContent = md;
|
|
196
|
+
// Nothing answered yet — show the empty serialisation as feedback but
|
|
197
|
+
// don't download a useless file.
|
|
198
|
+
if (entries.length > 0 || approval) downloadUserResponse(md, runMeta);
|
|
152
199
|
return md;
|
|
153
200
|
}
|
|
154
201
|
|
|
@@ -200,6 +247,7 @@
|
|
|
200
247
|
window.okstraReportView = {
|
|
201
248
|
buildUserResponseMarkdown: buildUserResponseMarkdown,
|
|
202
249
|
collectEntries: collectEntries,
|
|
250
|
+
collectApproval: collectApproval,
|
|
203
251
|
exportUserResponse: exportUserResponse,
|
|
204
252
|
};
|
|
205
253
|
}
|
|
@@ -62,6 +62,21 @@ created-at: 2026-05-17T10:00:00Z
|
|
|
62
62
|
> (prediction=0: 1,204) (prediction=1: 38)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## APPROVAL 블록 (implementation-planning 전용)
|
|
66
|
+
|
|
67
|
+
Plan Approval 위젯에서 승인을 체크하고 Export 하면 본문 끝에 다음 블록이 추가됩니다:
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
## APPROVAL
|
|
71
|
+
- Approved: true
|
|
72
|
+
- Implementation-Option: <Option Candidates 의 name 그대로>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- 옵션을 추천(권장) 그대로 두면 `Implementation-Option:` 라인은 생략됩니다 — implementation 은 plan 의 Recommended Option 으로 폴백합니다.
|
|
76
|
+
- 이 블록의 소비자는 implementation 시작 마법사의 approve-confirm 단계입니다 (`scripts/okstra_ctl/wizard.py`). 마법사가 사용자 확인 후 기존 `--approve` / `--implementation-option` 경로로 적용하며, sidecar 자체는 승인 게이트 검증을 우회하지 않습니다.
|
|
77
|
+
- 파서는 `scripts/okstra_ctl/user_response.py` `parse_user_response_approval` 이며, producer 출력과 byte-identical 한 소문자 `Approved: true` 만 인정합니다 (손편집된 `TRUE` 등은 fail-closed 로 불인정).
|
|
78
|
+
- `--resume-clarification` 은 sidecar 를 verbatim 첨부하므로 APPROVAL 블록도 그대로 실립니다 — planning 재실행 입력에서는 무해한 잔류 정보입니다.
|
|
79
|
+
|
|
65
80
|
## 호환성 규칙
|
|
66
81
|
|
|
67
82
|
- `Kind` 가 알 수 없는 값이면 폼은 `<textarea>` fallback 으로 렌더되며, 직렬화 시 받은 `Kind` 문자열을 그대로 보존합니다.
|
|
@@ -12,6 +12,13 @@ from dataclasses import dataclass
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import List, Tuple
|
|
14
14
|
|
|
15
|
+
# scripts/ is not a package; insert it so okstra_ctl is importable directly.
|
|
16
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
|
17
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
19
|
+
|
|
20
|
+
from okstra_ctl.md_table import split_pipe_row # noqa: E402
|
|
21
|
+
|
|
15
22
|
STAGE_MAP_HEADING = re.compile(r"^##\s+5\.5\s+Stage\s+Map\b", re.M)
|
|
16
23
|
REQUIRED_SUBSECTIONS = (
|
|
17
24
|
"Carry-In",
|
|
@@ -60,7 +67,7 @@ def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]
|
|
|
60
67
|
break
|
|
61
68
|
if not line.strip().startswith("|"):
|
|
62
69
|
continue
|
|
63
|
-
cells =
|
|
70
|
+
cells = split_pipe_row(line)
|
|
64
71
|
if len(cells) != 5:
|
|
65
72
|
continue
|
|
66
73
|
# skip header and separator rows (all-dash of any length is covered by set check)
|
|
@@ -119,7 +126,7 @@ def _effective_step_rows(section: str) -> List[List[str]]:
|
|
|
119
126
|
continue
|
|
120
127
|
if not s.startswith("|"):
|
|
121
128
|
continue
|
|
122
|
-
cells =
|
|
129
|
+
cells = split_pipe_row(s)
|
|
123
130
|
first_cell = cells[0]
|
|
124
131
|
if first_cell.lower() == "step":
|
|
125
132
|
continue
|
|
@@ -35,6 +35,7 @@ from okstra_ctl.clarification_items import ( # noqa: E402
|
|
|
35
35
|
)
|
|
36
36
|
from okstra_ctl.report_views import ( # noqa: E402
|
|
37
37
|
extract_html_digest,
|
|
38
|
+
plan_approval_context,
|
|
38
39
|
source_digest,
|
|
39
40
|
)
|
|
40
41
|
|
|
@@ -99,7 +100,7 @@ def validate(report_path: Path) -> list[str]:
|
|
|
99
100
|
# html sibling (see report_views.report_has_clarification_items). When
|
|
100
101
|
# there are no C-* rows and no html, that is the expected skip state.
|
|
101
102
|
if not html_path.is_file():
|
|
102
|
-
if not md_ids:
|
|
103
|
+
if not md_ids and plan_approval_context(report_path, md) is None:
|
|
103
104
|
return []
|
|
104
105
|
return [f"missing html artifact: {html_path}"]
|
|
105
106
|
|
|
@@ -40,6 +40,10 @@ from okstra_ctl.conformance import ( # noqa: E402
|
|
|
40
40
|
qa_result_from_dict,
|
|
41
41
|
validate_conformance_manifest,
|
|
42
42
|
)
|
|
43
|
+
from okstra_ctl.md_table import ( # noqa: E402
|
|
44
|
+
is_separator_row as _is_markdown_separator,
|
|
45
|
+
split_pipe_row as _split_pipe_row,
|
|
46
|
+
)
|
|
43
47
|
|
|
44
48
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
45
49
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
@@ -624,7 +628,7 @@ def _scan_token_usage_summary(content: str, failures: list[str]) -> None:
|
|
|
624
628
|
# Skip non-table lines, the header separator (`|------|`), and
|
|
625
629
|
# blank lines. Header rows have no backticks so they self-skip.
|
|
626
630
|
continue
|
|
627
|
-
cells =
|
|
631
|
+
cells = _split_pipe_row(line)
|
|
628
632
|
if not cells:
|
|
629
633
|
continue
|
|
630
634
|
label_cell = cells[0].strip("* `")
|
|
@@ -1214,27 +1218,12 @@ _CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
|
|
|
1214
1218
|
|
|
1215
1219
|
|
|
1216
1220
|
def _split_markdown_row(line: str) -> list[str]:
|
|
1217
|
-
stripped = line.strip()
|
|
1218
|
-
if stripped.startswith("|"):
|
|
1219
|
-
stripped = stripped[1:]
|
|
1220
|
-
if stripped.endswith("|"):
|
|
1221
|
-
stripped = stripped[:-1]
|
|
1222
1221
|
return [
|
|
1223
1222
|
_CELL_ANCHOR_RE.sub("", cell).strip().strip("`").strip()
|
|
1224
|
-
for cell in
|
|
1223
|
+
for cell in _split_pipe_row(line)
|
|
1225
1224
|
]
|
|
1226
1225
|
|
|
1227
1226
|
|
|
1228
|
-
def _is_markdown_separator(line: str) -> bool:
|
|
1229
|
-
stripped = line.strip()
|
|
1230
|
-
if not stripped.startswith("|"):
|
|
1231
|
-
return False
|
|
1232
|
-
for cell in stripped.strip("|").split("|"):
|
|
1233
|
-
if not re.fullmatch(r"\s*:?-{2,}:?\s*", cell):
|
|
1234
|
-
return False
|
|
1235
|
-
return True
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
1227
|
def _append_requirement_coverage_failures(
|
|
1239
1228
|
content: str,
|
|
1240
1229
|
gate_value: str,
|