okstra 0.149.0 → 0.150.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 (53) hide show
  1. package/docs/architecture.md +1 -1
  2. package/docs/cli.md +1 -1
  3. package/docs/project-structure-overview.md +1 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/agents/workers/translator-worker.md +67 -0
  7. package/runtime/bin/okstra-render-final-report.py +0 -11
  8. package/runtime/bin/okstra-report-translate.py +158 -0
  9. package/runtime/prompts/lead/adapters/claude-code.md +1 -1
  10. package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
  11. package/runtime/prompts/lead/report-writer.md +14 -13
  12. package/runtime/prompts/lead/team-contract.md +2 -2
  13. package/runtime/prompts/wizard/prompts.ko.json +17 -1
  14. package/runtime/python/okstra_ctl/analysis_inputs.py +24 -9
  15. package/runtime/python/okstra_ctl/analysis_packet.py +23 -1
  16. package/runtime/python/okstra_ctl/clarification_items.py +241 -44
  17. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -1
  18. package/runtime/python/okstra_ctl/dispatch_core.py +2 -2
  19. package/runtime/python/okstra_ctl/dispatch_state.py +12 -1
  20. package/runtime/python/okstra_ctl/final_report_paths.py +22 -1
  21. package/runtime/python/okstra_ctl/i18n.py +12 -7
  22. package/runtime/python/okstra_ctl/render_final_report.py +18 -17
  23. package/runtime/python/okstra_ctl/report_html/filters.py +15 -77
  24. package/runtime/python/okstra_ctl/report_html/render.py +44 -2
  25. package/runtime/python/okstra_ctl/report_translation.py +440 -0
  26. package/runtime/python/okstra_ctl/report_views.py +23 -9
  27. package/runtime/python/okstra_ctl/run.py +1 -1
  28. package/runtime/python/okstra_ctl/user_response.py +11 -6
  29. package/runtime/python/okstra_ctl/wizard.py +100 -25
  30. package/runtime/python/okstra_ctl/worker_liveness.py +130 -36
  31. package/runtime/templates/reports/html/base.template.html +12 -12
  32. package/runtime/templates/reports/html/i18n/en.json +395 -0
  33. package/runtime/templates/reports/html/i18n/ko.json +395 -0
  34. package/runtime/templates/reports/html/macros/forms.html +16 -16
  35. package/runtime/templates/reports/html/macros/visualizations.html +2 -2
  36. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +17 -17
  37. package/runtime/templates/reports/html/tasks/error-analysis.template.html +12 -12
  38. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +16 -16
  39. package/runtime/templates/reports/html/tasks/final-verification.template.html +12 -12
  40. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +37 -37
  41. package/runtime/templates/reports/html/tasks/implementation.template.html +18 -18
  42. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +7 -7
  43. package/runtime/templates/reports/html/tasks/project-analysis.template.html +29 -29
  44. package/runtime/templates/reports/html/tasks/release-handoff.template.html +13 -13
  45. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +14 -14
  46. package/runtime/templates/reports/report.js +8 -5
  47. package/runtime/validators/validate-report-views.py +1 -1
  48. package/runtime/validators/validate-run.py +28 -31
  49. package/src/cli-registry.mjs +11 -0
  50. package/src/commands/inspect/worker-liveness.mjs +9 -7
  51. package/src/commands/report/translate.mjs +31 -0
  52. package/src/lib/helper-scripts.mjs +1 -0
  53. package/runtime/templates/reports/i18n/ko.json +0 -273
@@ -4,7 +4,7 @@
4
4
 
5
5
  {% block human_content %}
6
6
  <section data-report-section="project-overview">
7
- <h2>Project overview</h2>
7
+ <h2>{{ t('tasks.project-analysis.project-overview') }}</h2>
8
8
  {{ render_narrative(narrative.projectOverview, "projectAnalysis.userNarrative.projectOverview") }}
9
9
  <div class="summary-grid">
10
10
  {% for reason in humanSummary.whyItMatters %}{{ summary_card("", reason, "important") }}{% endfor %}
@@ -12,63 +12,63 @@
12
12
  </section>
13
13
 
14
14
  <section data-report-section="architecture" data-report-field="projectAnalysis.components">
15
- <h2>Architecture and dependencies</h2>
15
+ <h2>{{ t('tasks.project-analysis.architecture-and-dependencies') }}</h2>
16
16
  {{ render_narrative(narrative.architectureExplanation, "projectAnalysis.userNarrative.architectureExplanation") }}
17
17
  {{ figure(architectureFigure, anchor_nodes=true) }}
18
18
  </section>
19
19
 
20
20
  <section data-report-section="infrastructure">
21
- <h2>Infrastructure</h2>
21
+ <h2>{{ t('tasks.project-analysis.infrastructure') }}</h2>
22
22
  {{ figure(infrastructureFigure) }}
23
23
  </section>
24
24
 
25
25
  {% if project.get("techStack") %}
26
26
  <section data-report-section="tech-stack" data-report-field="projectAnalysis.techStack">
27
- <h2>Tech stack</h2>
28
- <h3>Language</h3>
29
- <table><thead><tr><th>Language</th><th>Version</th><th>Version source</th><th>Files</th><th>Lines</th></tr></thead><tbody>{% for row in project.techStack.languages %}<tr><td>{{ row.name | inline_code }}</td><td><code>{{ row.version }}</code></td><td>{{ row.versionSource | enum_label("versionSource") }}</td><td>{{ row.get("fileCount") | default("--") }}</td><td>{{ row.get("lineCount") | default("--") }}</td></tr>{% endfor %}</tbody></table>
30
- {% if project.techStack.get("frameworks") %}<h3>Framework</h3>
31
- <table><thead><tr><th>Framework</th><th>Role</th><th>Evidence</th></tr></thead><tbody>{% for row in project.techStack.frameworks %}<tr>{{ row_key(pairs=[("Name", row.name), ("Version", row.version), ("Version source", row.versionSource | enum_label("versionSource"))]) }}<td>{{ row.role | inline_code }}</td><td>{{ row.get("currentCodeEvidence") | default([]) | code_evidence }}</td></tr>{% endfor %}</tbody></table>{% endif %}
32
- {% if project.techStack.get("toolchain") %}<h3>Tool</h3>
33
- <table><thead><tr><th>Tool</th><th>Command</th></tr></thead><tbody>{% for row in project.techStack.toolchain %}<tr>{{ row_key(pairs=[("Name", row.name), ("Kind", row.kind | enum_label("toolKind")), ("Version", row.get("version"))]) }}<td>{% if row.get("command") %}<code>{{ row.command }}</code>{% else %}--{% endif %}</td></tr>{% endfor %}</tbody></table>{% endif %}
27
+ <h2>{{ t('tasks.project-analysis.tech-stack') }}</h2>
28
+ <h3>{{ t('tasks.project-analysis.language') }}</h3>
29
+ <table><thead><tr><th>{{ t('tasks.project-analysis.language') }}</th><th>{{ t('tasks.project-analysis.version') }}</th><th>{{ t('tasks.project-analysis.version-source') }}</th><th>{{ t('tasks.project-analysis.files') }}</th><th>{{ t('tasks.project-analysis.lines') }}</th></tr></thead><tbody>{% for row in project.techStack.languages %}<tr><td>{{ row.name | inline_code }}</td><td><code>{{ row.version }}</code></td><td>{{ row.versionSource | enum_label("versionSource") }}</td><td>{{ row.get("fileCount") | default("--") }}</td><td>{{ row.get("lineCount") | default("--") }}</td></tr>{% endfor %}</tbody></table>
30
+ {% if project.techStack.get("frameworks") %}<h3>{{ t('tasks.project-analysis.framework') }}</h3>
31
+ <table><thead><tr><th>{{ t('tasks.project-analysis.framework') }}</th><th>{{ t('tasks.project-analysis.role') }}</th><th>{{ t('tasks.project-analysis.evidence') }}</th></tr></thead><tbody>{% for row in project.techStack.frameworks %}<tr>{{ row_key(pairs=[("Name", row.name), ("Version", row.version), ("Version source", row.versionSource | enum_label("versionSource"))]) }}<td>{{ row.role | inline_code }}</td><td>{{ row.get("currentCodeEvidence") | default([]) | code_evidence }}</td></tr>{% endfor %}</tbody></table>{% endif %}
32
+ {% if project.techStack.get("toolchain") %}<h3>{{ t('tasks.project-analysis.tool') }}</h3>
33
+ <table><thead><tr><th>{{ t('tasks.project-analysis.tool') }}</th><th>{{ t('tasks.project-analysis.command') }}</th></tr></thead><tbody>{% for row in project.techStack.toolchain %}<tr>{{ row_key(pairs=[("Name", row.name), ("Kind", row.kind | enum_label("toolKind")), ("Version", row.get("version"))]) }}<td>{% if row.get("command") %}<code>{{ row.command }}</code>{% else %}--{% endif %}</td></tr>{% endfor %}</tbody></table>{% endif %}
34
34
  </section>
35
35
  {% endif %}
36
36
 
37
37
  {% if project.get("internalInterfaces") %}
38
38
  <section data-report-section="internal-interfaces" data-report-field="projectAnalysis.internalInterfaces">
39
- <h2>Internal interfaces</h2>
40
- <table><thead><tr><th>Interface</th><th>Signature</th><th>Consumers</th><th>Evidence</th></tr></thead><tbody>{% for row in project.internalInterfaces %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Name", row.name), ("Kind", row.kind | enum_label("interfaceKind")), ("Owner", row.ownerComponentId)]) }}<td><code>{{ row.signature }}</code></td><td>{{ (row.get("consumers") | default([])) | join(", ") | inline_code }}</td><td>{{ row.get("currentCodeEvidence") | default([]) | code_evidence }}</td></tr>{% endfor %}</tbody></table>
39
+ <h2>{{ t('tasks.project-analysis.internal-interfaces') }}</h2>
40
+ <table><thead><tr><th>{{ t('tasks.project-analysis.interface') }}</th><th>{{ t('tasks.project-analysis.signature') }}</th><th>{{ t('tasks.project-analysis.consumers') }}</th><th>{{ t('tasks.project-analysis.evidence') }}</th></tr></thead><tbody>{% for row in project.internalInterfaces %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Name", row.name), ("Kind", row.kind | enum_label("interfaceKind")), ("Owner", row.ownerComponentId)]) }}<td><code>{{ row.signature }}</code></td><td>{{ (row.get("consumers") | default([])) | join(", ") | inline_code }}</td><td>{{ row.get("currentCodeEvidence") | default([]) | code_evidence }}</td></tr>{% endfor %}</tbody></table>
41
41
  </section>
42
42
  {% endif %}
43
43
 
44
44
  {% if project.get("workflows") %}
45
45
  <section data-report-section="workflows" data-report-field="projectAnalysis.workflows">
46
- <h2>Workflows</h2>
46
+ <h2>{{ t('tasks.project-analysis.workflows') }}</h2>
47
47
  {{ figure(workflowFigure) }}
48
48
  {% for flow in project.workflows %}
49
49
  <article class="workflow" id="id-{{ flow.id }}">
50
50
  <p class="eyebrow">{{ flow.id }}</p>
51
51
  <h3>{{ flow.name | inline_code }}</h3>
52
- <p><strong>Starts with</strong> — {{ flow.trigger | inline_code }}{% if flow.get("outcome") %} · <strong>Result</strong> — {{ flow.outcome | inline_code }}{% endif %}</p>
53
- <table><thead><tr><th>Order</th><th>Action</th><th>Component</th><th>External system</th></tr></thead><tbody>{% for step in flow.steps %}<tr><td>{{ step.order }}</td><td>{{ step.action | inline_code }}</td><td>{{ step.componentId | inline_code }}</td><td>{% if step.get("externalSystem") %}{{ step.externalSystem | inline_code }}{% else %}--{% endif %}</td></tr>{% endfor %}</tbody></table>
52
+ <p><strong>{{ t('tasks.project-analysis.starts-with') }}</strong> — {{ flow.trigger | inline_code }}{% if flow.get("outcome") %} · <strong>{{ t('tasks.project-analysis.result') }}</strong> — {{ flow.outcome | inline_code }}{% endif %}</p>
53
+ <table><thead><tr><th>{{ t('tasks.project-analysis.order') }}</th><th>{{ t('tasks.project-analysis.action') }}</th><th>{{ t('tasks.project-analysis.component') }}</th><th>{{ t('tasks.project-analysis.external-system') }}</th></tr></thead><tbody>{% for step in flow.steps %}<tr><td>{{ step.order }}</td><td>{{ step.action | inline_code }}</td><td>{{ step.componentId | inline_code }}</td><td>{% if step.get("externalSystem") %}{{ step.externalSystem | inline_code }}{% else %}--{% endif %}</td></tr>{% endfor %}</tbody></table>
54
54
  </article>
55
55
  {% endfor %}
56
56
  </section>
57
57
  {% endif %}
58
58
 
59
59
  <section data-report-section="boundaries">
60
- <h2>System boundaries</h2>
61
- <p class="scan-coverage" data-report-field="projectAnalysis.scanCoverage"><strong>{{ project.scanCoverage.scannedPathCount }}</strong> paths read · <strong>{{ project.scanCoverage.unscannedPathCount }}</strong> left unread — {{ project.scanCoverage.reason | inline_code }}</p>
62
- <h3>Entry point</h3>
63
- <table data-report-field="projectAnalysis.entryPoints"><thead><tr><th>Entry point</th><th>Path</th><th>Role</th></tr></thead><tbody>{% for row in project.entryPoints %}<tr>{{ row_key(pairs=[("Symbol", row.symbol), ("Kind", row.kind)]) }}<td><code>{{ row.path }}</code></td><td>{{ row.role | inline_code }}</td></tr>{% endfor %}</tbody></table>
64
- <h3>Data stores</h3>
65
- <table data-report-field="projectAnalysis.dataStores"><thead><tr><th>Store</th><th>Reads</th><th>Writes</th><th>Evidence</th></tr></thead><tbody>{% for row in project.dataStores %}<tr>{{ row_key(pairs=[("Name", row.name), ("Owner", row.ownerComponentId)]) }}<td>{{ row.readBoundary | inline_code }}</td><td>{{ row.writeBoundary | inline_code }}</td><td>{{ row.currentCodeEvidence | code_evidence }}</td></tr>{% endfor %}</tbody></table>
66
- <h3>External system</h3>
67
- <table data-report-field="projectAnalysis.externalSystems"><thead><tr><th>External system</th><th>Evidence</th></tr></thead><tbody>{% for row in project.externalSystems %}<tr>{{ row_key(pairs=[("Name", row.name), ("Adapter", row.adapter), ("Direction", row.direction)]) }}<td>{{ row.currentCodeEvidence | code_evidence }}</td></tr>{% endfor %}</tbody></table>
60
+ <h2>{{ t('tasks.project-analysis.system-boundaries') }}</h2>
61
+ <p class="scan-coverage" data-report-field="projectAnalysis.scanCoverage"><strong>{{ project.scanCoverage.scannedPathCount }}</strong> {{ t('tasks.project-analysis.paths-read') }} <strong>{{ project.scanCoverage.unscannedPathCount }}</strong> left unread — {{ project.scanCoverage.reason | inline_code }}</p>
62
+ <h3>{{ t('tasks.project-analysis.entry-point') }}</h3>
63
+ <table data-report-field="projectAnalysis.entryPoints"><thead><tr><th>{{ t('tasks.project-analysis.entry-point') }}</th><th>{{ t('tasks.project-analysis.path') }}</th><th>{{ t('tasks.project-analysis.role') }}</th></tr></thead><tbody>{% for row in project.entryPoints %}<tr>{{ row_key(pairs=[("Symbol", row.symbol), ("Kind", row.kind)]) }}<td><code>{{ row.path }}</code></td><td>{{ row.role | inline_code }}</td></tr>{% endfor %}</tbody></table>
64
+ <h3>{{ t('tasks.project-analysis.data-stores') }}</h3>
65
+ <table data-report-field="projectAnalysis.dataStores"><thead><tr><th>{{ t('tasks.project-analysis.store') }}</th><th>{{ t('tasks.project-analysis.reads') }}</th><th>{{ t('tasks.project-analysis.writes') }}</th><th>{{ t('tasks.project-analysis.evidence') }}</th></tr></thead><tbody>{% for row in project.dataStores %}<tr>{{ row_key(pairs=[("Name", row.name), ("Owner", row.ownerComponentId)]) }}<td>{{ row.readBoundary | inline_code }}</td><td>{{ row.writeBoundary | inline_code }}</td><td>{{ row.currentCodeEvidence | code_evidence }}</td></tr>{% endfor %}</tbody></table>
66
+ <h3>{{ t('tasks.project-analysis.external-system') }}</h3>
67
+ <table data-report-field="projectAnalysis.externalSystems"><thead><tr><th>{{ t('tasks.project-analysis.external-system') }}</th><th>{{ t('tasks.project-analysis.evidence') }}</th></tr></thead><tbody>{% for row in project.externalSystems %}<tr>{{ row_key(pairs=[("Name", row.name), ("Adapter", row.adapter), ("Direction", row.direction)]) }}<td>{{ row.currentCodeEvidence | code_evidence }}</td></tr>{% endfor %}</tbody></table>
68
68
  </section>
69
69
 
70
70
  <section data-report-section="hotspots" data-report-field="projectAnalysis.rankedHotspots">
71
- <h2>Hotspots to look at first</h2>
71
+ <h2>{{ t('tasks.project-analysis.hotspots-to-look-at-first') }}</h2>
72
72
  {{ render_narrative(narrative.hotspotExplanation, "projectAnalysis.userNarrative.hotspotExplanation") }}
73
73
  <div class="summary-grid">
74
74
  {% for row in project.rankedHotspots %}
@@ -78,20 +78,20 @@
78
78
  </section>
79
79
 
80
80
  <section data-report-section="quality-and-tests" data-report-field="projectAnalysis.qualityCoverage">
81
- <h2>Quality and test coverage</h2>
81
+ <h2>{{ t('tasks.project-analysis.quality-and-test-coverage') }}</h2>
82
82
  {{ render_narrative(narrative.qualityExplanation, "projectAnalysis.userNarrative.qualityExplanation") }}
83
- <table><thead><tr><th>Area</th><th>Detail</th><th>Evidence</th></tr></thead><tbody>{% for row in project.qualityCoverage %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Area", row.area)], status_name="Status", status_raw=row.status, status_text=row.status | enum_label("coverage")) }}<td>{{ row.summary | inline_code }}</td><td>{{ row.evidenceRefs | evidence_refs }}</td></tr>{% endfor %}</tbody></table>
83
+ <table><thead><tr><th>{{ t('tasks.project-analysis.area') }}</th><th>{{ t('tasks.project-analysis.detail') }}</th><th>{{ t('tasks.project-analysis.evidence') }}</th></tr></thead><tbody>{% for row in project.qualityCoverage %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Area", row.area)], status_name="Status", status_raw=row.status, status_text=row.status | enum_label("coverage")) }}<td>{{ row.summary | inline_code }}</td><td>{{ row.evidenceRefs | evidence_refs }}</td></tr>{% endfor %}</tbody></table>
84
84
  <dl class="enum-legend">{% for label, hint in "coverage" | enum_legend %}<dt>{{ label }}</dt><dd>{{ hint }}</dd>{% endfor %}</dl>
85
85
  </section>
86
86
 
87
87
  <section data-report-section="feature-map" data-report-field="projectAnalysis.featureIndex">
88
- <h2>Feature map</h2>
88
+ <h2>{{ t('tasks.project-analysis.feature-map') }}</h2>
89
89
  <div class="summary-grid">{% for row in project.featureIndex %}{{ summary_card(row.id ~ " · " ~ (row.name | inline_code), row.description, anchor=row.id) }}{% endfor %}</div>
90
90
  </section>
91
91
 
92
92
  <section data-report-section="user-actions">
93
- <h2>What to do next</h2>
93
+ <h2>{{ t('tasks.project-analysis.what-to-do-next') }}</h2>
94
94
  {{ action_list(humanSummary.actions) }}
95
- {% if humanSummary.blockers %}<h3>Blockers</h3><ul>{% for item in humanSummary.blockers %}<li>{{ item | inline_code }}</li>{% endfor %}</ul>{% endif %}
95
+ {% if humanSummary.blockers %}<h3>{{ t('tasks.project-analysis.blockers') }}</h3><ul>{% for item in humanSummary.blockers %}<li>{{ item | inline_code }}</li>{% endfor %}</ul>{% endif %}
96
96
  </section>
97
97
  {% endblock %}
@@ -4,46 +4,46 @@
4
4
 
5
5
  {% block human_content %}
6
6
  <section data-report-section="handoff-outcome">
7
- <h2>Handoff outcome</h2>
7
+ <h2>{{ t('tasks.release-handoff.handoff-outcome') }}</h2>
8
8
  {{ render_narrative(narrative.handoffOutcome, "releaseHandoff.userNarrative.handoffOutcome") }}
9
9
  {{ figure(handoffFigure) }}
10
10
  </section>
11
11
 
12
12
  {% if handoff.get("handoffScope") %}
13
13
  <section data-report-section="handoff-scope" data-report-field="releaseHandoff.handoffScope">
14
- <h2>Handoff scope</h2>
14
+ <h2>{{ t('tasks.release-handoff.handoff-scope') }}</h2>
15
15
  <p><strong>{{ handoff.handoffScope.mode | enum_label("handoffMode") }}</strong>{% if handoff.handoffScope.get("stages") %} — stages {{ handoff.handoffScope.stages | join(", ") }}{% endif %}{% if handoff.handoffScope.get("collectorBranch") %} · Collector branch <code>{{ handoff.handoffScope.collectorBranch }}</code>{% endif %}</p>
16
16
  </section>
17
17
  {% endif %}
18
18
 
19
19
  <section data-report-section="user-selection" data-report-field="releaseHandoff.userSelections">
20
- <h2>The behaviour you chose</h2>
21
- <dl><dt>Handoff mode</dt><dd>{{ handoff.userSelections.h1 }}</dd><dt>Conflict check</dt><dd>{{ handoff.userSelections.h2b }}</dd><dt>PR Body</dt><dd>{{ handoff.userSelections.h3 }}</dd></dl>
20
+ <h2>{{ t('tasks.release-handoff.the-behaviour-you-chose') }}</h2>
21
+ <dl><dt>{{ t('tasks.release-handoff.handoff-mode') }}</dt><dd>{{ handoff.userSelections.h1 }}</dd><dt>{{ t('tasks.release-handoff.conflict-check') }}</dt><dd>{{ handoff.userSelections.h2b }}</dd><dt>{{ t('tasks.release-handoff.pr-body') }}</dt><dd>{{ handoff.userSelections.h3 }}</dd></dl>
22
22
  </section>
23
23
 
24
24
  <section data-report-section="branch-and-pr" data-report-field="releaseHandoff.featureBranchState">
25
- <h2>Branch and PR</h2>
25
+ <h2>{{ t('tasks.release-handoff.branch-and-pr') }}</h2>
26
26
  {{ render_narrative(narrative.branchAndPrExplanation, "releaseHandoff.userNarrative.branchAndPrExplanation") }}
27
- <p><strong>Branch:</strong> {{ handoff.featureBranchState.branchName }}</p>
28
- <p data-report-field="releaseHandoff.pullRequestOutcome"><strong>PR Result:</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">Pull request Open</a>{% endif %}</p>
27
+ <p><strong>{{ t('tasks.release-handoff.branch') }}</strong> {{ handoff.featureBranchState.branchName }}</p>
28
+ <p data-report-field="releaseHandoff.pullRequestOutcome"><strong>{{ t('tasks.release-handoff.pr-result') }}</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">{{ t('tasks.release-handoff.pull-request-open') }}</a>{% endif %}</p>
29
29
  </section>
30
30
 
31
31
  <section data-report-section="conflicts" data-report-field="releaseHandoff.mergeConflictProbe">
32
- <h2>Conflict verdict</h2>
32
+ <h2>{{ t('tasks.release-handoff.conflict-verdict') }}</h2>
33
33
  {{ render_narrative(narrative.conflictExplanation, "releaseHandoff.userNarrative.conflictExplanation") }}
34
34
  {{ summary_card(handoff.mergeConflictProbe.kind, "Base branch: " ~ (handoff.mergeConflictProbe.baseBranch | default('not-run'))) }}
35
35
  {% if handoff.mergeConflictProbe.conflictingPaths %}<ul>{% for path in handoff.mergeConflictProbe.conflictingPaths %}<li>{{ path }}</li>{% endfor %}</ul>{% endif %}
36
36
  </section>
37
37
 
38
38
  <section data-report-section="delivered-commits" data-report-field="releaseHandoff.commitList">
39
- <h2>Commits delivered</h2>
40
- {% if handoff.commitList is mapping %}<p>There are no commits.</p>{% else %}
41
- <table><thead><tr><th>Commit</th><th>Subject</th><th>Files</th></tr></thead><tbody>{% for row in handoff.commitList %}<tr>{{ row_key(pairs=[("SHA", row.shortSha), ("Order", loop.index)]) }}<td>{{ row.subject | inline_code }}</td><td>{% for file in row.files %}<code>{{ file }}</code>{% if not loop.last %} {% endif %}{% endfor %}</td></tr>{% endfor %}</tbody></table>{% endif %}
42
- <p data-report-field="releaseHandoff.sourceVerificationReport"><strong>The verification report this rests on</strong> — <code>{{ handoff.sourceVerificationReport.path }}</code> · {{ handoff.sourceVerificationReport.verdictTokenQuote | inline_code }}</p>
39
+ <h2>{{ t('tasks.release-handoff.commits-delivered') }}</h2>
40
+ {% if handoff.commitList is mapping %}<p>{{ t('tasks.release-handoff.there-are-no-commits') }}</p>{% else %}
41
+ <table><thead><tr><th>{{ t('tasks.release-handoff.commit') }}</th><th>{{ t('tasks.release-handoff.subject') }}</th><th>{{ t('tasks.release-handoff.files') }}</th></tr></thead><tbody>{% for row in handoff.commitList %}<tr>{{ row_key(pairs=[("SHA", row.shortSha), ("Order", loop.index)]) }}<td>{{ row.subject | inline_code }}</td><td>{% for file in row.files %}<code>{{ file }}</code>{% if not loop.last %} {% endif %}{% endfor %}</td></tr>{% endfor %}</tbody></table>{% endif %}
42
+ <p data-report-field="releaseHandoff.sourceVerificationReport"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong> — <code>{{ handoff.sourceVerificationReport.path }}</code> · {{ handoff.sourceVerificationReport.verdictTokenQuote | inline_code }}</p>
43
43
  </section>
44
44
 
45
45
  <section data-report-section="next-action">
46
- <h2>What to do next</h2>
46
+ <h2>{{ t('tasks.release-handoff.what-to-do-next') }}</h2>
47
47
  {{ render_narrative(narrative.nextAction, "releaseHandoff.userNarrative.nextAction") }}
48
48
  <p>{{ handoff.routingRecommendation | inline_code }}</p>
49
49
  </section>
@@ -4,20 +4,20 @@
4
4
 
5
5
  {% block human_content %}
6
6
  <section data-report-section="request">
7
- <h2>The request as understood</h2>
7
+ <h2>{{ t('tasks.requirements-discovery.the-request-as-understood') }}</h2>
8
8
  {{ render_narrative(narrative.requestUnderstanding, "requirementsDiscovery.userNarrative.requestUnderstanding") }}
9
- <h3>Your request, verbatim</h3>
9
+ <h3>{{ t('tasks.requirements-discovery.your-request-verbatim') }}</h3>
10
10
  <p data-report-field="requirementsDiscovery.requestVerbatim">{{ requirements.requestVerbatim | inline_code }}</p>
11
- <h3>How the system reads it</h3>
11
+ <h3>{{ t('tasks.requirements-discovery.how-the-system-reads-it') }}</h3>
12
12
  <p data-report-field="requirementsDiscovery.systemInterpretation">{{ requirements.systemInterpretation | inline_code }}</p>
13
13
  <dl data-report-field="requirementsDiscovery.classification">{% for key, value in requirements.classification.items() %}<dt>{{ key }}</dt><dd>{{ value | inline_code }}</dd>{% endfor %}</dl>
14
14
  </section>
15
15
 
16
16
  {% if requirements.get("rejectionCriteria") %}
17
17
  <section data-report-section="rejection-criteria" data-report-field="requirementsDiscovery.rejectionCriteria">
18
- <h2>What would prove this wrong</h2>
18
+ <h2>{{ t('tasks.requirements-discovery.what-would-prove-this-wrong') }}</h2>
19
19
  {% if requirements.rejectionCriteria.source == "absent-not-material" %}
20
- <p>The requester left no rejection criteria, even if there had been, neither the classification nor the next step would change.</p>
20
+ <p>{{ t('tasks.requirements-discovery.the-requester-left-no-rejection-criteria-eve') }}</p>
21
21
  {% else %}
22
22
  <p>{{ requirements.rejectionCriteria.get("statement") | inline_code }}</p>
23
23
  <p class="evidence-refs">Source: {{ requirements.rejectionCriteria.source | enum_label("rejectionSource") }}{% if requirements.rejectionCriteria.get("clarificationId") %} · {{ requirements.rejectionCriteria.clarificationId | inline_code }}{% endif %}{% if requirements.rejectionCriteria.get("evidenceRefs") %} · {{ requirements.rejectionCriteria.evidenceRefs | evidence_refs }}{% endif %}</p>
@@ -27,31 +27,31 @@
27
27
 
28
28
  {% if requirements.get("domainAlignment") %}
29
29
  <section data-report-section="domain-alignment" data-report-field="requirementsDiscovery.domainAlignment">
30
- <h2>Terms settled</h2>
30
+ <h2>{{ t('tasks.requirements-discovery.terms-settled') }}</h2>
31
31
  <p>Glossary {{ "checked" if requirements.domainAlignment.glossaryConsulted else "none" }} · Decision record {{ "checked" if requirements.domainAlignment.decisionsConsulted else "none" }}</p>
32
- <table><thead><tr><th>As the brief put it</th><th>Settled term</th><th>Settled by</th><th>Evidence</th></tr></thead><tbody>{% for row in requirements.domainAlignment.terms %}<tr><td>{{ row.raw | inline_code }}</td><td>{{ row.canonical | inline_code }}</td><td>{{ row.resolvedBy | enum_label("termSource") }}</td><td>{{ row.get("evidenceRefs") | default([]) | evidence_refs }}</td></tr>{% else %}<tr><td colspan="4">No term was ambiguous.</td></tr>{% endfor %}</tbody></table>
32
+ <table><thead><tr><th>{{ t('tasks.requirements-discovery.as-the-brief-put-it') }}</th><th>{{ t('tasks.requirements-discovery.settled-term') }}</th><th>{{ t('tasks.requirements-discovery.settled-by') }}</th><th>{{ t('tasks.requirements-discovery.evidence') }}</th></tr></thead><tbody>{% for row in requirements.domainAlignment.terms %}<tr><td>{{ row.raw | inline_code }}</td><td>{{ row.canonical | inline_code }}</td><td>{{ row.resolvedBy | enum_label("termSource") }}</td><td>{{ row.get("evidenceRefs") | default([]) | evidence_refs }}</td></tr>{% else %}<tr><td colspan="4">{{ t('tasks.requirements-discovery.no-term-was-ambiguous') }}</td></tr>{% endfor %}</tbody></table>
33
33
  </section>
34
34
  {% endif %}
35
35
 
36
36
  <section data-report-section="requirements">
37
- <h2>Settled and unsettled requirements</h2>
37
+ <h2>{{ t('tasks.requirements-discovery.settled-and-unsettled-requirements') }}</h2>
38
38
  {{ render_narrative(narrative.confirmedRequirements, "requirementsDiscovery.userNarrative.confirmedRequirements") }}
39
39
  <div class="summary-grid" data-report-field="requirementsDiscovery.confirmedRequirements">{% for row in requirements.confirmedRequirements %}{{ summary_card(row.id ~ " · Settled", row.statement, anchor=row.id) }}{% endfor %}</div>
40
40
  {{ render_narrative(narrative.unresolvedRequirements, "requirementsDiscovery.userNarrative.unresolvedRequirements") }}
41
- <div class="summary-grid" data-report-field="requirementsDiscovery.unresolvedRequirements">{% for row in requirements.unresolvedRequirements %}{{ summary_card(row.id ~ " · Decision needed", row.statement, "important") }}{% else %}<p>Nothing is left unsettled.</p>{% endfor %}</div>
41
+ <div class="summary-grid" data-report-field="requirementsDiscovery.unresolvedRequirements">{% for row in requirements.unresolvedRequirements %}{{ summary_card(row.id ~ " · Decision needed", row.statement, "important") }}{% else %}<p>{{ t('tasks.requirements-discovery.nothing-is-left-unsettled') }}</p>{% endfor %}</div>
42
42
  </section>
43
43
 
44
44
  <section data-report-section="routing">
45
- <h2>Next tasks and dependencies</h2>
45
+ <h2>{{ t('tasks.requirements-discovery.next-tasks-and-dependencies') }}</h2>
46
46
  {{ render_narrative(narrative.routingExplanation, "requirementsDiscovery.userNarrative.routingExplanation") }}
47
- <p><strong>Next task:</strong> {{ requirements.routing.nextTaskType }} · {{ requirements.routing.rationale | inline_code }}</p>
47
+ <p><strong>{{ t('tasks.requirements-discovery.next-task') }}</strong> {{ requirements.routing.nextTaskType }} · {{ requirements.routing.rationale | inline_code }}</p>
48
48
  {{ figure(routingFigure) }}
49
- {% if not requirements.taskDependencies %}<p class="empty-state">This requirements task stands alone; nothing else feeds it.</p>{% endif %}
49
+ {% if not requirements.taskDependencies %}<p class="empty-state">{{ t('tasks.requirements-discovery.this-requirements-task-stands-alone-nothing') }}</p>{% endif %}
50
50
  </section>
51
51
 
52
52
  <section data-report-section="decisions">
53
- <h2>Decisions that are yours</h2>
54
- <div class="summary-grid">{% for row in clarificationItems %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>No further decision is needed.</p>{% endfor %}</div>
53
+ <h2>{{ t('tasks.requirements-discovery.decisions-that-are-yours') }}</h2>
54
+ <div class="summary-grid">{% for row in clarificationItems %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>{{ t('tasks.requirements-discovery.no-further-decision-is-needed') }}</p>{% endfor %}</div>
55
55
  {{ action_list(humanSummary.actions) }}
56
56
  </section>
57
57
  {% endblock %}
@@ -331,10 +331,7 @@
331
331
  if (/Reader Summary|Verdict Card|Clarification Items|Recommended Next Steps|Follow-up Tasks/.test(text)) {
332
332
  return "action";
333
333
  }
334
- // "토큰 사용량" is tokenSummary.heading from templates/reports/i18n/ko.json
335
- // the only audit heading the ko dictionary translates away from its English
336
- // form. tests/report/test_report_views.py pins the two in sync.
337
- if (/Evidence|Cross Verification|Execution Status|Token Usage|토큰 사용량|Plan Body Verification|Round History/.test(text)) {
334
+ if (/Evidence|Cross Verification|Execution Status|Token Usage|Plan Body Verification|Round History/.test(text)) {
338
335
  return "audit";
339
336
  }
340
337
  return "detail";
@@ -356,7 +353,13 @@
356
353
  // the run-metadata list and table of contents that trail it are document
357
354
  // chrome — classifying them would hide the title in action mode, because
358
355
  // the title text ("… Cross Verification Final Report") reads as audit.
359
- if ((node.tagName || "") === "H2") {
356
+ // A section that declares its own kind wins. Heading text is a fallback:
357
+ // the HTML's headings render in the reader's language, so "Evidence
358
+ // ledger" reads as audit and "근거 대장" reads as nothing at all.
359
+ var declared = node.getAttribute && node.getAttribute("data-reader-kind");
360
+ if (declared) {
361
+ current = declared;
362
+ } else if ((node.tagName || "") === "H2") {
360
363
  current = readerSectionKind(node.textContent || "");
361
364
  }
362
365
  node.setAttribute("data-reader-section", current);
@@ -227,7 +227,7 @@ def validate(report_path: Path) -> list[str]:
227
227
  if not html_path.is_file():
228
228
  if (
229
229
  not md_ids
230
- and plan_approval_context(report_path, md) is None
230
+ and plan_approval_context(report_path) is None
231
231
  and review_ctx is None
232
232
  ):
233
233
  return []
@@ -7058,42 +7058,39 @@ def _rerender_report_views_after_autofix(report_path: Path) -> str:
7058
7058
  """Re-render the ``*.html`` sibling against the just-substituted MD.
7059
7059
  Returns a short status note for the autofix message (empty on no-op,
7060
7060
  descriptive on failure).
7061
+
7062
+ Delegates to ``scripts/okstra-render-report-views.py`` rather than calling
7063
+ a renderer directly: that script is the single reference point that routes
7064
+ schema-v2 reports to the data.json-driven task view and schema-v1 reports
7065
+ to the legacy markdown renderer. Rendering here would have to re-derive
7066
+ both the routing and the run-meta, and getting either wrong overwrites a
7067
+ schema-v2 report's HTML — the user's only way to answer its clarifications
7068
+ — with a legacy render that carries no answer controls.
7061
7069
  """
7070
+ import subprocess
7071
+
7062
7072
  if not report_path.is_file():
7063
7073
  return ""
7064
- try:
7065
- # Late import — keeps validate-run.py importable in environments
7066
- # that don't ship report_views (older installs).
7067
- scripts_dir = Path(__file__).resolve().parent.parent / "scripts"
7068
- if str(scripts_dir) not in sys.path:
7069
- sys.path.insert(0, str(scripts_dir))
7070
- from okstra_ctl.report_views import RunMeta, render_html_view
7071
- templates_dir = (
7072
- Path(__file__).resolve().parent.parent / "templates" / "reports"
7073
- )
7074
- css = (templates_dir / "report.css").read_text(encoding="utf-8")
7075
- js = (templates_dir / "report.js").read_text(encoding="utf-8")
7076
- except Exception as exc: # noqa: BLE001 — best-effort
7077
- return f"report-views re-render skipped ({exc})"
7078
-
7079
- # Infer task-key / task-type / seq from path + body when possible;
7080
- # fall back to placeholders so the digest comparison (the actual
7081
- # contract) still works.
7082
- text = report_path.read_text(encoding="utf-8")
7083
- task_type_m = re.search(r"^- Task Type:\s*(\S+)", text, re.MULTILINE)
7084
- task_key_m = re.search(r"^- Task Key:\s*(\S+)", text, re.MULTILINE)
7085
- seq_m = re.search(r"-(\d+)\.md$", report_path.name)
7086
- meta = RunMeta(
7087
- task_key=task_key_m.group(1) if task_key_m else "unknown",
7088
- task_type=task_type_m.group(1) if task_type_m else "unknown",
7089
- seq=seq_m.group(1) if seq_m else "000",
7090
- source_report=report_path.name,
7074
+ renderer = (
7075
+ Path(__file__).resolve().parent.parent
7076
+ / "scripts" / "okstra-render-report-views.py"
7091
7077
  )
7078
+ if not renderer.is_file():
7079
+ return f"report-views re-render skipped (renderer missing under {renderer.parent})"
7092
7080
  try:
7093
- rendered = render_html_view(report_path, run_meta=meta, css=css, js=js)
7094
- except Exception as exc: # noqa: BLE001
7095
- return f"report-views re-render failed: {exc}"
7096
- if rendered is None:
7081
+ proc = subprocess.run(
7082
+ [sys.executable, str(renderer), str(report_path)],
7083
+ capture_output=True,
7084
+ text=True,
7085
+ timeout=60,
7086
+ )
7087
+ except subprocess.TimeoutExpired:
7088
+ return "report-views re-render failed: renderer timed out (60s)"
7089
+ if proc.returncode != 0:
7090
+ detail = (proc.stderr or proc.stdout).strip().splitlines()
7091
+ return f"report-views re-render failed: {detail[-1] if detail else 'unknown error'}"
7092
+ emitted = (proc.stdout or "").strip().splitlines()
7093
+ if emitted and emitted[-1].startswith("html: skipped"):
7097
7094
  return "report-views skipped (no §1 clarification rows)"
7098
7095
  return "report-views re-rendered"
7099
7096
 
@@ -333,6 +333,17 @@ export const COMMAND_REGISTRY = [
333
333
  category: "introspection",
334
334
  summary: ["Render slim AI + self-contained HTML views of a final report"],
335
335
  },
336
+ {
337
+ name: "report-translate",
338
+ module: "./commands/report/translate.mjs",
339
+ export: "run",
340
+ category: "introspection",
341
+ summary: [
342
+ "Build and verify the translation sidecar the HTML",
343
+ "renderer overlays when the report language is not",
344
+ "English (extract | check).",
345
+ ],
346
+ },
336
347
  {
337
348
  name: "report-finalize",
338
349
  module: "./commands/report/finalize.mjs",
@@ -3,15 +3,17 @@ import { runPythonModule } from "../../lib/python-helper.mjs";
3
3
  const USAGE = `okstra worker-liveness — report whether pending workers are still alive
4
4
 
5
5
  Usage:
6
- okstra worker-liveness [--audit <path>]...
7
- [--team-state <path> --worker <worker-id>]...
6
+ okstra worker-liveness [--team-state <path> --worker <worker-id>]...
8
7
  [--max-idle <seconds>] [--launch-grace <seconds>] [--json]
9
8
 
10
- --audit an in-process worker audit sidecar path; stale past the heartbeat cadence
11
- means the worker hung.
12
- --team-state and --worker select a CLI-wrapper assignment. No prompt sibling
13
- .log or .status.json past the launch grace means the wrapper never
14
- started. The grace begins at workers[].startedAt, not prompt mtime.
9
+ --team-state and --worker select one pending worker. The worker row's
10
+ livenessMode picks the probe: audit-heartbeat reads the in-process
11
+ worker audit sidecar path and reports stalled past the heartbeat
12
+ cadence; wrapper-status reports did-not-launch when no prompt sibling
13
+ .log or .status.json exists past the launch grace. Both graces begin
14
+ at workers[].startedAt, never at an artifact mtime — the audit sidecar
15
+ is reused on re-dispatch, so the previous attempt's last heartbeat is
16
+ not this dispatch's signal.
15
17
 
16
18
  Output: JSON { ok, checkedAt, probes[], unhealthy[] }. Exit 1 when any worker is
17
19
  stalled or did not launch, so a poll loop can branch on the exit code. Read-only:
@@ -0,0 +1,31 @@
1
+ import { runInstalledScript } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra report-translate — build and verify a final-report translation sidecar
4
+
5
+ A thin spawn shim over \`scripts/okstra-report-translate.py\` (installed at
6
+ \`$HOME/.okstra/bin/okstra-report-translate.py\`).
7
+
8
+ The final-report data.json is authored in English and stays the SSOT. When the
9
+ resolved report language is not English, Phase 7 dispatches a translator that
10
+ writes a sidecar beside the report; \`render-views\` overlays it so only the
11
+ human HTML is localized.
12
+
13
+ Usage:
14
+ okstra report-translate extract <path-to-final-report.data.json>
15
+ Write <stem>.translation-source.json — every pointer the report holds a
16
+ translatable string at, paired with its English text. The translator
17
+ fills in the values, so a sidecar cannot cite a path the report lacks.
18
+
19
+ okstra report-translate check <path-to-final-report.i18n.<lang>.json>
20
+ Resolve every pointer in a filled sidecar against the report it names
21
+ and report what is still English. Exits non-zero on a pointer that
22
+ resolves nowhere — the translator's own gate before it returns.
23
+ `;
24
+
25
+ export async function run(args) {
26
+ return runInstalledScript({
27
+ scriptName: "okstra-report-translate.py",
28
+ args,
29
+ usage: USAGE,
30
+ });
31
+ }
@@ -12,6 +12,7 @@ export const HELPER_SCRIPT_NAMES = Object.freeze([
12
12
  "okstra-inject-report-index.py",
13
13
  "okstra-render-final-report.py",
14
14
  "okstra-render-report-views.py",
15
+ "okstra-report-translate.py",
15
16
  "okstra-spawn-followups.py",
16
17
  "okstra-token-usage.py",
17
18
  ]);