okstra 0.149.0 → 0.151.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 (57) hide show
  1. package/docs/architecture.md +1 -1
  2. package/docs/cli.md +2 -2
  3. package/docs/project-structure-overview.md +1 -1
  4. package/package.json +3 -2
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/agents/workers/report-writer-worker.md +8 -0
  7. package/runtime/agents/workers/translator-worker.md +67 -0
  8. package/runtime/bin/okstra-render-final-report.py +0 -11
  9. package/runtime/bin/okstra-report-translate.py +191 -0
  10. package/runtime/prompts/lead/adapters/claude-code.md +1 -1
  11. package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
  12. package/runtime/prompts/lead/plan-body-verification.md +1 -1
  13. package/runtime/prompts/lead/report-writer.md +16 -15
  14. package/runtime/prompts/lead/team-contract.md +2 -2
  15. package/runtime/prompts/wizard/prompts.ko.json +17 -1
  16. package/runtime/python/okstra_ctl/analysis_inputs.py +24 -9
  17. package/runtime/python/okstra_ctl/analysis_packet.py +23 -1
  18. package/runtime/python/okstra_ctl/clarification_items.py +241 -44
  19. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -1
  20. package/runtime/python/okstra_ctl/convergence.py +15 -1
  21. package/runtime/python/okstra_ctl/dispatch_core.py +2 -2
  22. package/runtime/python/okstra_ctl/dispatch_state.py +12 -1
  23. package/runtime/python/okstra_ctl/final_report_paths.py +22 -1
  24. package/runtime/python/okstra_ctl/i18n.py +12 -7
  25. package/runtime/python/okstra_ctl/render_final_report.py +18 -17
  26. package/runtime/python/okstra_ctl/report_finalize.py +18 -0
  27. package/runtime/python/okstra_ctl/report_html/filters.py +15 -77
  28. package/runtime/python/okstra_ctl/report_html/render.py +44 -2
  29. package/runtime/python/okstra_ctl/report_translation.py +469 -0
  30. package/runtime/python/okstra_ctl/report_views.py +23 -9
  31. package/runtime/python/okstra_ctl/run.py +1 -1
  32. package/runtime/python/okstra_ctl/user_response.py +11 -6
  33. package/runtime/python/okstra_ctl/wizard.py +100 -25
  34. package/runtime/python/okstra_ctl/worker_liveness.py +130 -36
  35. package/runtime/templates/reports/html/base.template.html +12 -12
  36. package/runtime/templates/reports/html/i18n/en.json +395 -0
  37. package/runtime/templates/reports/html/i18n/ko.json +395 -0
  38. package/runtime/templates/reports/html/macros/forms.html +16 -16
  39. package/runtime/templates/reports/html/macros/visualizations.html +2 -2
  40. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +17 -17
  41. package/runtime/templates/reports/html/tasks/error-analysis.template.html +12 -12
  42. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +16 -16
  43. package/runtime/templates/reports/html/tasks/final-verification.template.html +12 -12
  44. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +37 -37
  45. package/runtime/templates/reports/html/tasks/implementation.template.html +18 -18
  46. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +7 -7
  47. package/runtime/templates/reports/html/tasks/project-analysis.template.html +29 -29
  48. package/runtime/templates/reports/html/tasks/release-handoff.template.html +13 -13
  49. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +14 -14
  50. package/runtime/templates/reports/report.js +8 -5
  51. package/runtime/validators/validate-report-views.py +1 -1
  52. package/runtime/validators/validate-run.py +59 -31
  53. package/src/cli-registry.mjs +11 -0
  54. package/src/commands/inspect/worker-liveness.mjs +9 -7
  55. package/src/commands/report/translate.mjs +31 -0
  56. package/src/lib/helper-scripts.mjs +1 -0
  57. 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 []
@@ -61,6 +61,10 @@ from okstra_ctl.mutation_probe import ( # noqa: E402
61
61
  classify_reason,
62
62
  )
63
63
  from okstra_ctl.self_mock_signals import selfmock_path_key # noqa: E402
64
+ from okstra_ctl.report_translation import ( # noqa: E402
65
+ HANGUL_PROSE_LIMIT,
66
+ hangul_share,
67
+ )
64
68
  from okstra_ctl.stage_citations import cited_stage_numbers # noqa: E402
65
69
  from okstra_ctl.workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE # noqa: E402
66
70
  from okstra_ctl.md_table import ( # noqa: E402
@@ -6316,6 +6320,32 @@ def _validate_improvement_discovery(
6316
6320
  failures.append(f"improvement-discovery: {err}")
6317
6321
 
6318
6322
 
6323
+ def _validate_ssot_is_english(data: dict, failures: list[str]) -> None:
6324
+ """Enforce: the final-report data.json is authored in English.
6325
+
6326
+ `meta.reportLanguage` names the language the human HTML renders in, and
6327
+ Phase 7's translator serves it from a sidecar. The data.json itself is the
6328
+ record every later phase, validator and agent reads, so a worker that
6329
+ authors it in the reader's language instead splits the record — and does so
6330
+ silently, because rendering, follow-up spawning and validation all succeed
6331
+ on it.
6332
+
6333
+ `report-finalize` runs the same check as its first step, before anything
6334
+ derives from the report. This is the second gate, for a report that reached
6335
+ validation by some other path.
6336
+ """
6337
+ if not data:
6338
+ return
6339
+ share, length = hangul_share(data)
6340
+ if length and share >= HANGUL_PROSE_LIMIT:
6341
+ failures.append(
6342
+ f"final-report data.json was authored in Korean ({share:.0%} of its "
6343
+ f"prose, limit {HANGUL_PROSE_LIMIT:.0%}). The data.json is the "
6344
+ "English SSOT; meta.reportLanguage selects the human HTML's "
6345
+ "language and is served by the Phase 7 translator sidecar."
6346
+ )
6347
+
6348
+
6319
6349
  def _validate_fix_cycle(run_manifest: dict, data: dict, failures: list[str]) -> None:
6320
6350
  """Enforce: when the run-manifest carries a fixCycleId, the final-report
6321
6351
  data.json MUST contain a fixCycle block whose ``cycle`` matches it.
@@ -7058,42 +7088,39 @@ def _rerender_report_views_after_autofix(report_path: Path) -> str:
7058
7088
  """Re-render the ``*.html`` sibling against the just-substituted MD.
7059
7089
  Returns a short status note for the autofix message (empty on no-op,
7060
7090
  descriptive on failure).
7091
+
7092
+ Delegates to ``scripts/okstra-render-report-views.py`` rather than calling
7093
+ a renderer directly: that script is the single reference point that routes
7094
+ schema-v2 reports to the data.json-driven task view and schema-v1 reports
7095
+ to the legacy markdown renderer. Rendering here would have to re-derive
7096
+ both the routing and the run-meta, and getting either wrong overwrites a
7097
+ schema-v2 report's HTML — the user's only way to answer its clarifications
7098
+ — with a legacy render that carries no answer controls.
7061
7099
  """
7100
+ import subprocess
7101
+
7062
7102
  if not report_path.is_file():
7063
7103
  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,
7104
+ renderer = (
7105
+ Path(__file__).resolve().parent.parent
7106
+ / "scripts" / "okstra-render-report-views.py"
7091
7107
  )
7108
+ if not renderer.is_file():
7109
+ return f"report-views re-render skipped (renderer missing under {renderer.parent})"
7092
7110
  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:
7111
+ proc = subprocess.run(
7112
+ [sys.executable, str(renderer), str(report_path)],
7113
+ capture_output=True,
7114
+ text=True,
7115
+ timeout=60,
7116
+ )
7117
+ except subprocess.TimeoutExpired:
7118
+ return "report-views re-render failed: renderer timed out (60s)"
7119
+ if proc.returncode != 0:
7120
+ detail = (proc.stderr or proc.stdout).strip().splitlines()
7121
+ return f"report-views re-render failed: {detail[-1] if detail else 'unknown error'}"
7122
+ emitted = (proc.stdout or "").strip().splitlines()
7123
+ if emitted and emitted[-1].startswith("html: skipped"):
7097
7124
  return "report-views skipped (no §1 clarification rows)"
7098
7125
  return "report-views re-rendered"
7099
7126
 
@@ -7209,6 +7236,7 @@ def main() -> int:
7209
7236
  ),
7210
7237
  )
7211
7238
  validation_data = report_data if isinstance(report_data, Mapping) else {}
7239
+ _validate_ssot_is_english(validation_data, failures)
7212
7240
  _validate_fix_cycle(run_manifest, validation_data, failures)
7213
7241
  validate_report(
7214
7242
  report_path,
@@ -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
  ]);