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.
- package/README.md +23 -9
- package/docs/architecture/storage-model.md +39 -65
- package/docs/architecture.md +68 -60
- package/docs/cli.md +40 -23
- package/docs/for-ai/skills/okstra-run.md +13 -34
- package/docs/performance-improvement-plan-v2.md +2 -2
- package/docs/pr-template-usage.md +1 -1
- package/docs/project-structure-overview.md +26 -22
- package/docs/task-process/README.md +4 -4
- package/docs/task-process/common-flow.md +12 -12
- package/docs/task-process/final-verification.md +2 -2
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/package.json +2 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/agents/workers/grok-worker.md +256 -0
- package/runtime/agents/workers/kimi-worker.md +256 -0
- package/runtime/agents/workers/report-writer-worker.md +12 -12
- package/runtime/bin/lib/okstra/cli.sh +13 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/usage.sh +17 -12
- package/runtime/bin/okstra-grok-exec.sh +5 -0
- package/runtime/bin/okstra-kimi-exec.sh +5 -0
- package/runtime/bin/okstra-provider-exec.py +235 -0
- package/runtime/bin/okstra-render-final-report.py +4 -4
- package/runtime/bin/okstra-render-report-views.py +100 -12
- package/runtime/bin/okstra.sh +3 -0
- package/runtime/prompts/lead/adapters/antigravity.md +48 -0
- package/runtime/prompts/lead/adapters/claude-code.md +13 -11
- package/runtime/prompts/lead/adapters/codex.md +7 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
- package/runtime/prompts/lead/report-writer.md +16 -12
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +16 -10
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +12 -12
- package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
- package/runtime/prompts/profiles/error-analysis.md +2 -0
- package/runtime/prompts/profiles/feature-analysis.md +2 -0
- package/runtime/prompts/profiles/final-verification.md +3 -1
- package/runtime/prompts/profiles/forbidden-actions.json +4 -4
- package/runtime/prompts/profiles/implementation-planning.md +3 -1
- package/runtime/prompts/profiles/implementation.md +2 -2
- package/runtime/prompts/profiles/improvement-discovery.md +6 -2
- package/runtime/prompts/profiles/project-analysis.md +2 -0
- package/runtime/prompts/profiles/release-handoff.md +7 -7
- package/runtime/prompts/profiles/requirements-discovery.md +2 -0
- package/runtime/prompts/wizard/prompts.ko.json +9 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
- package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
- package/runtime/python/okstra_ctl/lead_events.py +1 -1
- package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
- package/runtime/python/okstra_ctl/models.py +156 -8
- package/runtime/python/okstra_ctl/path_hints.py +9 -25
- package/runtime/python/okstra_ctl/paths.py +1 -1
- package/runtime/python/okstra_ctl/render.py +172 -74
- package/runtime/python/okstra_ctl/render_final_report.py +136 -28
- package/runtime/python/okstra_ctl/report_contract.py +124 -0
- package/runtime/python/okstra_ctl/report_finalize.py +1 -1
- package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
- package/runtime/python/okstra_ctl/report_html/common.py +86 -0
- package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
- package/runtime/python/okstra_ctl/report_html/models.py +59 -0
- package/runtime/python/okstra_ctl/report_html/render.py +76 -0
- package/runtime/python/okstra_ctl/report_html/router.py +40 -0
- package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
- package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
- package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
- package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
- package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
- package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
- package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
- package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
- package/runtime/python/okstra_ctl/report_views.py +15 -43
- package/runtime/python/okstra_ctl/run.py +276 -51
- package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
- package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
- package/runtime/python/okstra_ctl/team.py +2 -7
- package/runtime/python/okstra_ctl/wizard.py +194 -21
- package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
- package/runtime/python/okstra_ctl/workers.py +3 -1
- package/runtime/python/okstra_ctl/workflow.py +4 -2
- package/runtime/python/okstra_token_usage/__init__.py +1 -0
- package/runtime/python/okstra_token_usage/collect.py +32 -23
- package/runtime/python/okstra_token_usage/pricing.py +35 -3
- package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
- package/runtime/skills/okstra-run/SKILL.md +31 -42
- package/runtime/templates/prd/pr-body.template.md +1 -1
- package/runtime/templates/reports/final-report-v2.template.md +66 -0
- package/runtime/templates/reports/html/assets/base.css +41 -0
- package/runtime/templates/reports/html/assets/base.js +5 -0
- package/runtime/templates/reports/html/base.template.html +79 -0
- package/runtime/templates/reports/html/macros/forms.html +47 -0
- package/runtime/templates/reports/html/macros/layout.html +19 -0
- package/runtime/templates/reports/html/macros/visualizations.html +27 -0
- package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
- package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
- package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
- package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
- package/runtime/templates/reports/report.js +21 -4
- package/runtime/templates/reports/settings.template.json +4 -0
- package/runtime/templates/reports/task-brief.template.md +7 -7
- package/runtime/validators/validate-report-views.py +86 -4
- package/runtime/validators/validate-run.py +73 -15
- package/runtime/validators/validate_improvement_report.py +55 -0
- package/runtime/validators/validate_session_conformance.py +2 -1
- package/src/cli-registry.mjs +4 -4
- package/src/commands/execute/codex-dispatch.mjs +7 -10
- package/src/commands/execute/render-bundle.mjs +3 -3
- package/src/commands/execute/run.mjs +17 -52
- package/src/commands/execute/wizard.mjs +4 -1
- package/src/commands/lifecycle/doctor.mjs +6 -3
- package/src/commands/lifecycle/install.mjs +49 -21
- package/src/commands/report/finalize.mjs +2 -3
- package/src/commands/report/render-final-report.mjs +4 -2
- package/src/commands/report/render-views.mjs +8 -8
- package/src/lib/runtime-manifest.mjs +1 -1
- package/src/lib/runtime-resolver.mjs +2 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
1214
|
-
if
|
|
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
|
|
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=
|
|
1237
|
-
recommended_option=
|
|
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
|
-
"""
|
|
1322
|
-
|
|
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:
|