okstra 0.146.1 → 0.148.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 (137) hide show
  1. package/README.md +23 -9
  2. package/docs/architecture/storage-model.md +39 -65
  3. package/docs/architecture.md +68 -60
  4. package/docs/cli.md +40 -23
  5. package/docs/for-ai/skills/okstra-run.md +13 -34
  6. package/docs/performance-improvement-plan-v2.md +2 -2
  7. package/docs/pr-template-usage.md +1 -1
  8. package/docs/project-structure-overview.md +26 -22
  9. package/docs/task-process/README.md +4 -4
  10. package/docs/task-process/common-flow.md +12 -12
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation.md +1 -1
  13. package/docs/task-process/release-handoff.md +1 -1
  14. package/package.json +2 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/agents/workers/antigravity-worker.md +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/codex-worker.md +2 -2
  19. package/runtime/agents/workers/grok-worker.md +256 -0
  20. package/runtime/agents/workers/kimi-worker.md +256 -0
  21. package/runtime/agents/workers/report-writer-worker.md +12 -12
  22. package/runtime/bin/lib/okstra/cli.sh +13 -1
  23. package/runtime/bin/lib/okstra/globals.sh +3 -0
  24. package/runtime/bin/lib/okstra/usage.sh +17 -12
  25. package/runtime/bin/okstra-grok-exec.sh +5 -0
  26. package/runtime/bin/okstra-kimi-exec.sh +5 -0
  27. package/runtime/bin/okstra-provider-exec.py +235 -0
  28. package/runtime/bin/okstra-render-final-report.py +4 -4
  29. package/runtime/bin/okstra-render-report-views.py +100 -12
  30. package/runtime/bin/okstra.sh +3 -0
  31. package/runtime/prompts/lead/adapters/antigravity.md +48 -0
  32. package/runtime/prompts/lead/adapters/claude-code.md +13 -11
  33. package/runtime/prompts/lead/adapters/codex.md +7 -7
  34. package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
  35. package/runtime/prompts/lead/report-writer.md +16 -12
  36. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  37. package/runtime/prompts/profiles/_common-contract.md +16 -10
  38. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  39. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  40. package/runtime/prompts/profiles/_implementation-executor.md +12 -12
  41. package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
  42. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  43. package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
  44. package/runtime/prompts/profiles/error-analysis.md +2 -0
  45. package/runtime/prompts/profiles/feature-analysis.md +2 -0
  46. package/runtime/prompts/profiles/final-verification.md +3 -1
  47. package/runtime/prompts/profiles/forbidden-actions.json +4 -4
  48. package/runtime/prompts/profiles/implementation-planning.md +3 -1
  49. package/runtime/prompts/profiles/implementation.md +2 -2
  50. package/runtime/prompts/profiles/improvement-discovery.md +6 -2
  51. package/runtime/prompts/profiles/project-analysis.md +2 -0
  52. package/runtime/prompts/profiles/release-handoff.md +7 -7
  53. package/runtime/prompts/profiles/requirements-discovery.md +2 -0
  54. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  55. package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
  56. package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
  57. package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
  58. package/runtime/python/okstra_ctl/lead_events.py +1 -1
  59. package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
  60. package/runtime/python/okstra_ctl/models.py +156 -8
  61. package/runtime/python/okstra_ctl/path_hints.py +9 -25
  62. package/runtime/python/okstra_ctl/paths.py +1 -1
  63. package/runtime/python/okstra_ctl/render.py +172 -74
  64. package/runtime/python/okstra_ctl/render_final_report.py +136 -28
  65. package/runtime/python/okstra_ctl/report_contract.py +124 -0
  66. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  67. package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
  68. package/runtime/python/okstra_ctl/report_html/common.py +86 -0
  69. package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
  70. package/runtime/python/okstra_ctl/report_html/models.py +59 -0
  71. package/runtime/python/okstra_ctl/report_html/render.py +76 -0
  72. package/runtime/python/okstra_ctl/report_html/router.py +40 -0
  73. package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
  74. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
  75. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
  76. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
  77. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
  78. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
  79. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
  80. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
  81. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
  82. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
  83. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
  84. package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
  85. package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
  86. package/runtime/python/okstra_ctl/report_views.py +15 -43
  87. package/runtime/python/okstra_ctl/run.py +276 -51
  88. package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
  89. package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
  90. package/runtime/python/okstra_ctl/team.py +2 -7
  91. package/runtime/python/okstra_ctl/wizard.py +194 -21
  92. package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
  93. package/runtime/python/okstra_ctl/workers.py +3 -1
  94. package/runtime/python/okstra_ctl/workflow.py +4 -2
  95. package/runtime/python/okstra_token_usage/__init__.py +1 -0
  96. package/runtime/python/okstra_token_usage/collect.py +32 -23
  97. package/runtime/python/okstra_token_usage/pricing.py +35 -3
  98. package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
  99. package/runtime/skills/okstra-run/SKILL.md +31 -42
  100. package/runtime/templates/prd/pr-body.template.md +1 -1
  101. package/runtime/templates/reports/final-report-v2.template.md +66 -0
  102. package/runtime/templates/reports/html/assets/base.css +41 -0
  103. package/runtime/templates/reports/html/assets/base.js +5 -0
  104. package/runtime/templates/reports/html/base.template.html +79 -0
  105. package/runtime/templates/reports/html/macros/forms.html +47 -0
  106. package/runtime/templates/reports/html/macros/layout.html +19 -0
  107. package/runtime/templates/reports/html/macros/visualizations.html +27 -0
  108. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
  109. package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
  110. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
  111. package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
  112. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
  113. package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
  114. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
  115. package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
  116. package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
  117. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
  118. package/runtime/templates/reports/report.js +21 -4
  119. package/runtime/templates/reports/settings.template.json +4 -0
  120. package/runtime/templates/reports/task-brief.template.md +7 -7
  121. package/runtime/validators/validate-report-views.py +86 -4
  122. package/runtime/validators/validate-run.py +73 -15
  123. package/runtime/validators/validate_improvement_report.py +55 -0
  124. package/runtime/validators/validate_session_conformance.py +2 -1
  125. package/src/cli-registry.mjs +4 -4
  126. package/src/commands/execute/codex-dispatch.mjs +7 -10
  127. package/src/commands/execute/render-bundle.mjs +3 -3
  128. package/src/commands/execute/run.mjs +17 -52
  129. package/src/commands/execute/wizard.mjs +4 -1
  130. package/src/commands/lifecycle/doctor.mjs +6 -3
  131. package/src/commands/lifecycle/install.mjs +49 -21
  132. package/src/commands/report/finalize.mjs +2 -3
  133. package/src/commands/report/render-final-report.mjs +4 -2
  134. package/src/commands/report/render-views.mjs +8 -8
  135. package/src/lib/runtime-manifest.mjs +1 -1
  136. package/src/lib/runtime-resolver.mjs +2 -2
  137. package/src/lib/worker-agent-render.mjs +50 -0
@@ -0,0 +1,139 @@
1
+ """Deterministic inline-SVG builders with table-compatible node IDs."""
2
+ from __future__ import annotations
3
+
4
+ import html
5
+ from dataclasses import replace
6
+ from typing import Iterable, Sequence
7
+
8
+ from .models import FigureModel, VisualEdge, VisualNode
9
+
10
+
11
+ _ROWS_PER_COLUMN = 8
12
+ _COLUMN_WIDTH = 260
13
+ _ROW_HEIGHT = 100
14
+
15
+
16
+ def _node_positions(nodes: Sequence[VisualNode]) -> dict[str, tuple[int, int]]:
17
+ """Lay out one column per group, wrapping tall groups into extra columns."""
18
+ groups = sorted({n.group for n in nodes})
19
+ counts: dict[str, int] = {}
20
+ column_offset: dict[str, int] = {}
21
+ offset = 0
22
+ for group in groups:
23
+ column_offset[group] = offset
24
+ size = sum(1 for n in nodes if n.group == group)
25
+ offset += max(1, -(-size // _ROWS_PER_COLUMN))
26
+ positions: dict[str, tuple[int, int]] = {}
27
+ for node in nodes:
28
+ index = counts.get(node.group, 0)
29
+ counts[node.group] = index + 1
30
+ column = column_offset[node.group] + index // _ROWS_PER_COLUMN
31
+ row = index % _ROWS_PER_COLUMN
32
+ positions[node.id] = (50 + column * _COLUMN_WIDTH, 55 + row * _ROW_HEIGHT)
33
+ return positions
34
+
35
+
36
+ _ARROW_MARKER = (
37
+ '<defs><marker id="edge-arrow" viewBox="0 0 10 10" refX="9" refY="5" '
38
+ 'markerWidth="6" markerHeight="6" orient="auto-start-reverse">'
39
+ '<path d="M 0 0 L 10 5 L 0 10 z" class="edge-arrow-head" /></marker></defs>'
40
+ )
41
+
42
+
43
+ def _svg_document(nodes: Sequence[VisualNode], edges: Sequence[VisualEdge]) -> str:
44
+ positions = _node_positions(nodes)
45
+ width = max((x for x, _ in positions.values()), default=50) + 230
46
+ height = max((y for _, y in positions.values()), default=55) + 90
47
+ parts = [f'<svg viewBox="0 0 {width} {height}" role="img" xmlns="http://www.w3.org/2000/svg">']
48
+ parts.append(_ARROW_MARKER)
49
+ for edge in edges:
50
+ if edge.source not in positions or edge.target not in positions:
51
+ continue
52
+ x1, y1 = positions[edge.source]
53
+ x2, y2 = positions[edge.target]
54
+ parts.append(
55
+ f'<g class="edge-group"><title>{html.escape(edge.label)}</title>'
56
+ f'<line x1="{x1 + 90}" y1="{y1 + 25}" x2="{x2 + 90}" y2="{y2 + 25}" '
57
+ f'class="edge edge-{html.escape(edge.kind)}" marker-end="url(#edge-arrow)" /></g>'
58
+ )
59
+ for node in nodes:
60
+ x, y = positions[node.id]
61
+ node_id = html.escape(node.id)
62
+ label = html.escape(node.label)
63
+ status = html.escape(node.status)
64
+ detail = html.escape(node.detail)
65
+ parts.append(
66
+ f'<g data-node-id="{node_id}" class="node node-{status}">'
67
+ f"<title>{label}: {status}. {detail}</title>"
68
+ f'<rect x="{x}" y="{y}" width="180" height="50" rx="8"/>'
69
+ f'<text x="{x + 12}" y="{y + 30}">{label}</text></g>'
70
+ )
71
+ parts.append("</svg>")
72
+ return "".join(parts)
73
+
74
+
75
+ def dependency_figure(*, nodes: Sequence[VisualNode], edges: Sequence[VisualEdge], title: str) -> FigureModel:
76
+ ordered_nodes = tuple(sorted(nodes, key=lambda node: node.id))
77
+ ordered_edges = tuple(sorted(edges, key=lambda edge: (edge.source, edge.target, edge.label)))
78
+ return FigureModel(
79
+ figure_id="dependency-graph",
80
+ kind="dependency",
81
+ title=title,
82
+ summary=f"{len(ordered_nodes)} nodes and {len(ordered_edges)} relationships",
83
+ nodes=ordered_nodes,
84
+ edges=ordered_edges,
85
+ svg=_svg_document(ordered_nodes, ordered_edges),
86
+ )
87
+
88
+
89
+ def flow_figure(*, nodes: Sequence[VisualNode], edges: Sequence[VisualEdge], title: str) -> FigureModel:
90
+ return replace(dependency_figure(nodes=nodes, edges=edges, title=title), kind="flow", figure_id="flow-graph")
91
+
92
+
93
+ def decision_flow_figure(*, nodes: Sequence[VisualNode], edges: Sequence[VisualEdge], title: str) -> FigureModel:
94
+ return replace(
95
+ dependency_figure(nodes=nodes, edges=edges, title=title),
96
+ kind="decision-flow",
97
+ figure_id="decision-flow",
98
+ )
99
+
100
+
101
+ def cause_graph_figure(*, nodes: Sequence[VisualNode], edges: Sequence[VisualEdge], title: str) -> FigureModel:
102
+ return replace(
103
+ dependency_figure(nodes=nodes, edges=edges, title=title),
104
+ kind="cause-graph",
105
+ figure_id="cause-graph",
106
+ )
107
+
108
+
109
+ def stage_map_figure(*, nodes: Sequence[VisualNode], edges: Sequence[VisualEdge], title: str) -> FigureModel:
110
+ return replace(
111
+ dependency_figure(nodes=nodes, edges=edges, title=title),
112
+ kind="stage-map",
113
+ figure_id="stage-map",
114
+ )
115
+
116
+
117
+ def change_map_figure(*, nodes: Sequence[VisualNode], title: str) -> FigureModel:
118
+ return replace(
119
+ dependency_figure(nodes=nodes, edges=(), title=title),
120
+ kind="change-map",
121
+ figure_id="change-map",
122
+ )
123
+
124
+
125
+ def matrix_figure(*, items: Iterable[VisualNode], title: str, x_label: str, y_label: str) -> FigureModel:
126
+ nodes = tuple(items)
127
+ summary = f"{len(nodes)} items compared by {x_label} and {y_label}"
128
+ return replace(dependency_figure(nodes=nodes, edges=(), title=title), kind="matrix", figure_id="matrix", summary=summary)
129
+
130
+
131
+ def coverage_figure(*, rows: Iterable[VisualNode], title: str) -> FigureModel:
132
+ nodes = tuple(rows)
133
+ return replace(dependency_figure(nodes=nodes, edges=(), title=title), kind="coverage", figure_id="coverage")
134
+
135
+
136
+ def timeline_figure(*, events: Iterable[VisualNode], title: str) -> FigureModel:
137
+ nodes = tuple(events)
138
+ edges = tuple(VisualEdge(nodes[i].id, nodes[i + 1].id, "next", "sequence") for i in range(len(nodes) - 1))
139
+ return replace(dependency_figure(nodes=nodes, edges=edges, title=title), kind="timeline", figure_id="timeline")
@@ -5,7 +5,10 @@ from pathlib import Path
5
5
 
6
6
 
7
7
  def html_view_path(report_path: Path) -> Path:
8
- """Return the self-contained HTML view path for a final-report markdown."""
8
+ """Return the HTML sibling for a final-report Markdown or data path."""
9
+ if report_path.name.endswith(".data.json"):
10
+ stem = report_path.name.removesuffix(".data.json")
11
+ return report_path.with_name(stem + ".html")
9
12
  return report_path.with_suffix(".html")
10
13
 
11
14
 
@@ -35,6 +35,11 @@ from pathlib import Path
35
35
  from typing import Iterable, Optional
36
36
 
37
37
  from .final_report_paths import final_report_data_path
38
+ from .report_html.view_models.implementation_planning import (
39
+ PlanApprovalState,
40
+ plan_approval_state,
41
+ resolve_recommended_option,
42
+ )
38
43
  from .report_view_artifacts import html_view_path, user_responses_dir_for_report
39
44
 
40
45
 
@@ -1142,16 +1147,7 @@ def report_has_clarification_items(src_md: str) -> bool:
1142
1147
  )
1143
1148
 
1144
1149
 
1145
- @dataclass(frozen=True)
1146
- class PlanApprovalContext:
1147
- """Plan Approval 위젯 렌더 입력. ``disabled_reason`` 이 비어 있지 않으면
1148
- 위젯은 disabled 로 렌더된다 (run-prep 승인 게이트와 동일한 fail-closed 기준).
1149
- ``blocker_ids`` 는 §1 미해소 승인-차단 항목 ID 목록 — 화면 안내가 사용자에게
1150
- '어느 항목을 답해야 하는지' 를 보여주는 데 쓴다 (unreadable 사유면 빈 튜플)."""
1151
- option_names: tuple[str, ...]
1152
- recommended_option: str
1153
- disabled_reason: str
1154
- blocker_ids: tuple[str, ...]
1150
+ PlanApprovalContext = PlanApprovalState
1155
1151
 
1156
1152
 
1157
1153
  def _load_report_data(src_md_path: Path) -> dict | None:
@@ -1210,31 +1206,21 @@ def plan_approval_context(
1210
1206
  data = _load_report_data(src_md_path)
1211
1207
  if data is None:
1212
1208
  return None
1213
- planning = data.get("implementationPlanning")
1214
- if not isinstance(planning, dict):
1209
+ state = plan_approval_state(data)
1210
+ if state is None:
1215
1211
  return None
1216
- names = tuple(
1217
- c.get("name", "")
1218
- for c in (planning.get("optionCandidates") or [])
1219
- if isinstance(c, dict) and c.get("name")
1220
- )
1221
- if not names:
1222
- return None
1223
- rec = planning.get("recommendedOption")
1224
- rec_name = rec.get("name") or "" if isinstance(rec, dict) else ""
1225
- recommended = _resolve_recommended_option(rec_name, names)
1226
1212
  scan = scan_approval_gate(src_text)
1227
- blocker_ids: tuple[str, ...] = ()
1213
+ blocker_ids = state.blocker_ids
1214
+ reason = state.disabled_reason
1228
1215
  if scan.unreadable_reason:
1216
+ blocker_ids = ()
1229
1217
  reason = "§1 승인 게이트를 읽을 수 없어 승인이 비활성화되었습니다 — 보고서를 재렌더하세요."
1230
1218
  elif scan.blockers:
1231
1219
  blocker_ids = tuple(b.row_id for b in scan.blockers)
1232
1220
  reason = f"§1 승인 차단 항목 {len(scan.blockers)}건 미해소"
1233
- else:
1234
- reason = ""
1235
1221
  return PlanApprovalContext(
1236
- option_names=names,
1237
- recommended_option=recommended,
1222
+ option_names=state.option_names,
1223
+ recommended_option=state.recommended_option,
1238
1224
  disabled_reason=reason,
1239
1225
  blocker_ids=blocker_ids,
1240
1226
  )
@@ -1318,22 +1304,8 @@ def _approval_label(data: dict, approval_ctx: PlanApprovalContext | None) -> str
1318
1304
 
1319
1305
 
1320
1306
  def _resolve_recommended_option(rec_name: str, names: tuple[str, ...]) -> str:
1321
- """``recommendedOption.name`` 을 후보 이름에 맞춘다. 정확 일치가 없으면
1322
- 접두 일치(후보가 ``(RECOMMENDED)`` 같은 표식 접미를 더 달고 있는 흔한 경우)를
1323
- 허용하되, 후보 이름이 서로의 접두여서 둘 이상이 매칭되면 모호하므로 채택하지
1324
- 않고 첫 후보로 폴백한다 — 임의의 한 후보를 권장으로 잘못 표시하느니 결정적인
1325
- 폴백이 낫다. 항상 정확히 한 옵션이 selected 가 되어 브라우저 자동선택분의 묵시
1326
- Export 를 차단한다."""
1327
- if rec_name in names:
1328
- return rec_name
1329
- if rec_name:
1330
- matches = [
1331
- name for name in names
1332
- if name.startswith(rec_name) or rec_name.startswith(name)
1333
- ]
1334
- if len(matches) == 1:
1335
- return matches[0]
1336
- return names[0]
1307
+ """Compatibility wrapper for the shared v2 approval-state resolver."""
1308
+ return resolve_recommended_option(rec_name, names)
1337
1309
 
1338
1310
 
1339
1311
  def _approval_blocked_guidance(ctx: PlanApprovalContext, run_meta: RunMeta) -> str: