okstra 0.163.2 → 0.165.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 +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +42 -7
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -67,6 +67,21 @@
|
|
|
67
67
|
"timeout": "시간 초과",
|
|
68
68
|
"not-run": "미실행",
|
|
69
69
|
"synthesis-only": "종합만 수행"
|
|
70
|
+
},
|
|
71
|
+
"replanMode": {
|
|
72
|
+
"full": "전부 다시 계획함",
|
|
73
|
+
"incremental": "일부만 다시 계획함"
|
|
74
|
+
},
|
|
75
|
+
"ledgerKind": {
|
|
76
|
+
"code-evidence": "코드 근거",
|
|
77
|
+
"hypothesis": "가설",
|
|
78
|
+
"confirmed-fact": "확인된 사실",
|
|
79
|
+
"inference": "추론",
|
|
80
|
+
"unknown": "미확인",
|
|
81
|
+
"cross-check-consensus": "작업자 간 합의",
|
|
82
|
+
"cross-check-dissent": "작업자 간 이견",
|
|
83
|
+
"missing-information": "빠진 정보",
|
|
84
|
+
"follow-up": "후속 작업"
|
|
70
85
|
}
|
|
71
86
|
},
|
|
72
87
|
"enumHint": {
|
|
@@ -85,7 +100,8 @@
|
|
|
85
100
|
"written": "작성",
|
|
86
101
|
"elapsed": "소요 시간",
|
|
87
102
|
"evidence-ledger": "근거 대장",
|
|
88
|
-
"source
|
|
103
|
+
"source": "출처",
|
|
104
|
+
"confidence": "확신도",
|
|
89
105
|
"export-my-answers": "내 답변 내보내기",
|
|
90
106
|
"copy": "복사",
|
|
91
107
|
"dismiss": "닫기",
|
|
@@ -95,7 +111,7 @@
|
|
|
95
111
|
},
|
|
96
112
|
"runUsage": {
|
|
97
113
|
"heading": "이번 실행에 든 비용",
|
|
98
|
-
"intro": "이번 실행이 투입한 에이전트별로 얼마를 썼고 얼마나 오래 일했는지입니다. 에이전트는 서로 겹쳐서 돌기 때문에 각 작업 시간을 더해도 머리말의 소요 시간과 같지 않습니다. 원시 토큰은
|
|
114
|
+
"intro": "이번 실행이 투입한 에이전트별로 얼마를 썼고 얼마나 오래 일했는지입니다. 에이전트는 서로 겹쳐서 돌기 때문에 각 작업 시간을 더해도 머리말의 소요 시간과 같지 않습니다. 원시 토큰은 이번 실행이 모델에 한 번씩 통과시킨 분량입니다. 캐시 재읽기는 매 턴 다시 읽어 들이는 앞선 대화 전체로, 입력 단가의 10분의 1로 매겨지기 때문에 원시 토큰 밖에 있으면서 금액 안에는 들어옵니다. 과금 환산 토큰은 이 둘을 모두 입력 단가 기준으로 환산한 값이고, 금액은 거기서 나옵니다.",
|
|
99
115
|
"agent": "에이전트",
|
|
100
116
|
"role": "역할",
|
|
101
117
|
"model": "모델",
|
|
@@ -109,7 +125,9 @@
|
|
|
109
125
|
"row-lead": "리드",
|
|
110
126
|
"row-workers": "워커 합계",
|
|
111
127
|
"row-total": "합계",
|
|
112
|
-
"row-cli": "CLI 호출 (별도 청구)"
|
|
128
|
+
"row-cli": "CLI 호출 (별도 청구)",
|
|
129
|
+
"cache-read-tokens": "캐시 재읽기",
|
|
130
|
+
"row-task-cumulative": "이 태스크 누적 (run {runs}회)"
|
|
113
131
|
},
|
|
114
132
|
"macros": {
|
|
115
133
|
"forms": {
|
|
@@ -122,7 +140,6 @@
|
|
|
122
140
|
"why": "이유",
|
|
123
141
|
"further-evidence": "추가 근거",
|
|
124
142
|
"scope-change-requested": "범위 변경 요청",
|
|
125
|
-
"approve-this-plan": "이 계획 승인하기",
|
|
126
143
|
"implementation-option": "구현 방식 선택",
|
|
127
144
|
"i-approve-this-plan": "이 계획을 승인합니다.",
|
|
128
145
|
"approval-is-held": "승인이 보류 중입니다:",
|
|
@@ -136,7 +153,12 @@
|
|
|
136
153
|
"recommended": "권장",
|
|
137
154
|
"scope-impact": "범위",
|
|
138
155
|
"added-work": "추가 작업",
|
|
139
|
-
"direction-change": "방향 전환"
|
|
156
|
+
"direction-change": "방향 전환",
|
|
157
|
+
"decide-on-this-plan": "이 계획에 대한 결정",
|
|
158
|
+
"request-changes-to-this-plan": "고쳐서 다시 가져오게 합니다",
|
|
159
|
+
"reject-this-plan": "이 계획을 반려합니다",
|
|
160
|
+
"why-required-for-changes-or-rejection": "사유 — 반려하거나 다시 고치게 할 때는 반드시 적어야 합니다",
|
|
161
|
+
"answer-the-blockers-then-regenerate": "{ids}에 답한 뒤 답변을 내보내고, clarification resume 명령으로 리포트를 다시 만드세요."
|
|
140
162
|
},
|
|
141
163
|
"visualizations": {
|
|
142
164
|
"component": "구성 요소",
|
|
@@ -285,7 +307,8 @@
|
|
|
285
307
|
"statement-overturned": "뒤집힌 진술",
|
|
286
308
|
"replaced-with": "대체된 내용",
|
|
287
309
|
"no-answer-overturned-anything": "답변으로 뒤집힌 것이 없습니다.",
|
|
288
|
-
"scope-restated": "다시
|
|
310
|
+
"scope-restated": "계획을 어디까지 다시 썼는지",
|
|
311
|
+
"scope-restated-lede": "이번 계획은 앞선 계획을 이어받아 시작했습니다. 답변을 반영한 뒤 그중 얼마가 살아남았는지입니다.",
|
|
289
312
|
"stages-verified-again": "다시 검증한 stage",
|
|
290
313
|
"stages-carried-over-as-they-were": "그대로 이어받은 stage",
|
|
291
314
|
"decision-record-draft": "결정 기록 초안",
|
|
@@ -297,7 +320,9 @@
|
|
|
297
320
|
"topic": "주제",
|
|
298
321
|
"why": "이유",
|
|
299
322
|
"open-decisions-before-approval": "승인 전에 남은 결정",
|
|
300
|
-
"no-open-decision-is-holding-approval": "승인을 막는 미결 결정이 없습니다."
|
|
323
|
+
"no-open-decision-is-holding-approval": "승인을 막는 미결 결정이 없습니다.",
|
|
324
|
+
"uniform-verifier-label": "단일 판정 검증자",
|
|
325
|
+
"uniform-verifier-legend": "이 검증자는 자기가 투표한 모든 항목에 같은 판정을 냈습니다. 그래서 이번 라운드의 반증 신호는 나머지 검증자에게서만 나왔고, 위의 게이트 값은 실제보다 넓은 교차 검증처럼 읽힙니다. 만장일치도 정당한 결과이므로, 그 워커의 audit 사이드카에서 인용한 근거를 실제로 열어봤는지 확인하십시오."
|
|
301
326
|
},
|
|
302
327
|
"implementation": {
|
|
303
328
|
"what-was-delivered": "무엇을 만들었나",
|
|
@@ -18,15 +18,21 @@
|
|
|
18
18
|
|
|
19
19
|
{% macro plan_approval(state) -%}
|
|
20
20
|
<section id="plan-approval" data-report-section="plan-approval">
|
|
21
|
-
<h2>{{ t('macros.forms.
|
|
21
|
+
<h2>{{ t('macros.forms.decide-on-this-plan') }}</h2>
|
|
22
|
+
<fieldset id="plan-decision"><legend>{{ t('macros.forms.verdict') }}</legend>
|
|
23
|
+
<input id="plan-decision-approved" type="radio" name="plan-decision-status" value="approved"{% if state.disabled_reason %} disabled{% endif %}><label for="plan-decision-approved">{{ t('macros.forms.i-approve-this-plan') }}</label>
|
|
24
|
+
<input id="plan-decision-revision-requested" type="radio" name="plan-decision-status" value="revision-requested"><label for="plan-decision-revision-requested">{{ t('macros.forms.request-changes-to-this-plan') }}</label>
|
|
25
|
+
<input id="plan-decision-rejected" type="radio" name="plan-decision-status" value="rejected"><label for="plan-decision-rejected">{{ t('macros.forms.reject-this-plan') }}</label>
|
|
26
|
+
</fieldset>
|
|
22
27
|
<label for="approval-option">{{ t('macros.forms.implementation-option') }}</label>
|
|
23
28
|
<select id="approval-option"{% if state.disabled_reason %} disabled{% endif %}>
|
|
24
29
|
{% for name in state.option_names %}
|
|
25
30
|
{% if name == state.recommended_option %}<option data-recommended="true" selected value="{{ name }}">{{ name | inline_code }} (recommended)</option>{% else %}<option value="{{ name }}">{{ name | inline_code }}</option>{% endif %}
|
|
26
31
|
{% endfor %}
|
|
27
32
|
</select>
|
|
28
|
-
<
|
|
29
|
-
|
|
33
|
+
<label for="plan-decision-reason">{{ t('macros.forms.why-required-for-changes-or-rejection') }}</label>
|
|
34
|
+
<textarea id="plan-decision-reason" rows="4"></textarea>
|
|
35
|
+
{% if state.disabled_reason %}<div class="approval-disabled-reason"><p>{{ t('macros.forms.approval-is-held') }} <strong>{{ state.disabled_reason }}</strong>.</p><p>{{ t('macros.forms.answer-the-blockers-then-regenerate') | replace('{ids}', state.blocker_ids | join(', ')) }}</p></div>{% endif %}
|
|
30
36
|
</section>
|
|
31
37
|
{%- endmacro %}
|
|
32
38
|
|
|
@@ -55,24 +55,16 @@
|
|
|
55
55
|
</section>
|
|
56
56
|
{% endif %}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</
|
|
67
|
-
|
|
68
|
-
<section data-report-section="plan-verification" data-report-field="implementationPlanning.planBodyVerification">
|
|
69
|
-
<h2>{{ t('tasks.implementation-planning.plan-body-verification') }}</h2>
|
|
70
|
-
<p><strong>{{ t('tasks.implementation-planning.verdict') }}</strong> — {{ planning.planBodyVerification.gateResult | inline_code }} ({{ planning.planBodyVerification.roundCount }} rounds{% if planning.planBodyVerification.get("selfFixRoundsApplied") is not none %}, {{ planning.planBodyVerification.selfFixRoundsApplied }} self-fix rounds{% if planning.planBodyVerification.get("selfFixStopReason") %} · {{ planning.planBodyVerification.selfFixStopReason | inline_code }}{% endif %}{% endif %})</p>
|
|
71
|
-
{% if planning.planBodyVerification.get("gateBlockedBy") %}<p><strong>{{ t('tasks.implementation-planning.what-it-blocked') }}</strong> — {{ planning.planBodyVerification.gateBlockedBy | join(", ") | inline_code }}</p>{% endif %}
|
|
72
|
-
{% if planning.planBodyVerification.get("uniformVerifiers") %}<p><strong>{{ t('implementationPlanning.planBodyUniformVerifierLabel') }}</strong> — {% for u in planning.planBodyVerification.uniformVerifiers %}{{ u.worker | inline_code }} → {{ u.verdict | inline_code }} ({{ u.itemCount }}){% if not loop.last %}, {% endif %}{% endfor %}<br><small>{{ t('implementationPlanning.planBodyUniformVerifierLegend') }}</small></p>{% endif %}
|
|
73
|
-
<table><thead><tr><th>{{ t('tasks.implementation-planning.plan-item') }}</th><th>{{ t('tasks.implementation-planning.subject') }}</th></tr></thead><tbody>{% for row in planning.planBodyVerification.planItems %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Source section", row.sourceSection)]) }}<td>{{ row.subject | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
74
|
-
{% if planning.planBodyVerification.get("dissentLog") %}<h3>{{ t('tasks.implementation-planning.dissent-left-standing') }}</h3>
|
|
75
|
-
<table><thead><tr><th>{{ t('tasks.implementation-planning.subject') }}</th><th>{{ t('tasks.implementation-planning.body') }}</th></tr></thead><tbody>{% for row in planning.planBodyVerification.dissentLog %}<tr>{{ row_key(pairs=[("Subject", row.planItem), ("Worker", row.workerRole)]) }}<td>{{ row.body | inline_code }}</td></tr>{% endfor %}</tbody></table>{% endif %}
|
|
58
|
+
{# The verification checklist, cross-project dependencies, migration risk,
|
|
59
|
+
rollback strategy and plan-body verification rounds are the implementer's
|
|
60
|
+
and the auditor's working material, not the approver's: this reader is
|
|
61
|
+
deciding whether to greenlight the plan, and none of those tables move that
|
|
62
|
+
decision. They stay in the markdown report, which is what the next phase and
|
|
63
|
+
the audit trail read. Requirement coverage is the exception — "does this plan
|
|
64
|
+
actually do what the brief asked" is the approval question itself. #}
|
|
65
|
+
<section data-report-section="requirement-coverage" data-report-field="implementationPlanning.requirementCoverage">
|
|
66
|
+
<h2>{{ t('tasks.implementation-planning.how-each-requirement-gets-met') }}</h2>
|
|
67
|
+
<table><thead><tr><th>{{ t('tasks.implementation-planning.requirement') }}</th><th>{{ t('tasks.implementation-planning.body') }}</th><th>{{ t('tasks.implementation-planning.covered-by') }}</th></tr></thead><tbody>{% for row in planning.requirementCoverage %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Source", row.source)], status_name="Status", status_raw=row.status, status_text=row.status) }}<td>{{ row.requirement | inline_code }}</td><td>{{ row.coveredBy | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
76
68
|
</section>
|
|
77
69
|
|
|
78
70
|
{% if planning.get("supersessionLedger") is not none %}
|
|
@@ -84,8 +76,11 @@
|
|
|
84
76
|
|
|
85
77
|
{% if planning.get("incrementalDecision") %}<section data-report-section="replan-scope" data-report-field="implementationPlanning.incrementalDecision">
|
|
86
78
|
<h2>{{ t('tasks.implementation-planning.scope-restated') }}</h2>
|
|
87
|
-
<p
|
|
79
|
+
<p class="replan-lede">{{ t('tasks.implementation-planning.scope-restated-lede') }}</p>
|
|
80
|
+
<p><strong>{{ planning.incrementalDecision.mode | enum_label('replanMode') }}</strong> — {{ planning.incrementalDecision.reason | inline_code }}</p>
|
|
81
|
+
{% if planning.incrementalDecision.reverifyStages or planning.incrementalDecision.carryStages %}
|
|
88
82
|
<p><strong>{{ t('tasks.implementation-planning.stages-verified-again') }}</strong> — {{ planning.incrementalDecision.reverifyStages | default([]) | join(", ") | inline_code }} · <strong>{{ t('tasks.implementation-planning.stages-carried-over-as-they-were') }}</strong> — {{ planning.incrementalDecision.carryStages | default([]) | join(", ") | inline_code }}</p>
|
|
83
|
+
{% endif %}
|
|
89
84
|
</section>{% endif %}
|
|
90
85
|
|
|
91
86
|
<section data-report-section="decision-drafts" data-report-field="implementationPlanning.decisionDrafts">
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* data-response-id> (everything else / fallback).
|
|
9
9
|
* 2. Serialise the entries into markdown whose bytes are IDENTICAL
|
|
10
10
|
* to scripts/okstra_ctl/report_views.py serialize_user_response.
|
|
11
|
-
* 3. Collect the
|
|
11
|
+
* 3. Collect the plan Approve / Request changes / Reject decision and the
|
|
12
|
+
* analysis-only Accept / Request revision / Reject control.
|
|
12
13
|
* 4. Write the result to <pre id="user-response-output">, offer a
|
|
13
14
|
* [Copy] button, and download it as
|
|
14
15
|
* user-response-<task-type>-<seq>.md — the user drops that file in
|
|
@@ -95,21 +96,43 @@
|
|
|
95
96
|
return entries;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
function validatePlanDecision(decision) {
|
|
100
|
+
if (!decision) return null;
|
|
101
|
+
var allowed = ["approved", "revision-requested", "rejected"];
|
|
102
|
+
if (allowed.indexOf(decision.status) < 0) {
|
|
103
|
+
throw new Error("PLAN DECISION Status is invalid");
|
|
104
|
+
}
|
|
105
|
+
if (decision.status !== "approved" && !trimMultiline(decision.reason)) {
|
|
106
|
+
throw new Error("PLAN DECISION " + decision.status + " requires a Reason");
|
|
107
|
+
}
|
|
108
|
+
return decision;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 계획 결정 위젯 (implementation-planning 보고서에만 존재). 아무 판정도
|
|
112
|
+
// 고르지 않았으면 null. 승인 라디오는 §1 차단이 남아 있으면 disabled 다.
|
|
113
|
+
// 구현 옵션은 승인일 때만 의미가 있고, 추천(data-recommended) 그대로면
|
|
114
|
+
// implementationOption 을 비워 보낸다 → 소비 측 Recommended 폴백.
|
|
115
|
+
function collectPlanDecision() {
|
|
116
|
+
var selected = document.querySelector(
|
|
117
|
+
'input[name="plan-decision-status"]:checked'
|
|
118
|
+
);
|
|
119
|
+
if (!selected || selected.disabled) return null;
|
|
105
120
|
var option = "";
|
|
106
|
-
if (
|
|
107
|
-
var
|
|
108
|
-
if (
|
|
109
|
-
|
|
121
|
+
if (selected.value === "approved") {
|
|
122
|
+
var sel = document.getElementById("approval-option");
|
|
123
|
+
if (sel && sel.selectedIndex >= 0) {
|
|
124
|
+
var opt = sel.options[sel.selectedIndex];
|
|
125
|
+
if (opt && opt.getAttribute("data-recommended") !== "true") {
|
|
126
|
+
option = opt.value;
|
|
127
|
+
}
|
|
110
128
|
}
|
|
111
129
|
}
|
|
112
|
-
|
|
130
|
+
var reason = document.getElementById("plan-decision-reason");
|
|
131
|
+
return validatePlanDecision({
|
|
132
|
+
status: selected.value,
|
|
133
|
+
implementationOption: option,
|
|
134
|
+
reason: reason ? trimMultiline(reason.value) : "",
|
|
135
|
+
});
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
function validateAnalysisReview(review) {
|
|
@@ -192,7 +215,20 @@
|
|
|
192
215
|
);
|
|
193
216
|
}
|
|
194
217
|
|
|
195
|
-
function
|
|
218
|
+
function serialisePlanDecision(decision) {
|
|
219
|
+
decision = validatePlanDecision(decision);
|
|
220
|
+
var chunk = "\n## PLAN DECISION\n- Status: " + decision.status + "\n";
|
|
221
|
+
if (decision.implementationOption) {
|
|
222
|
+
chunk +=
|
|
223
|
+
"- Implementation-Option: " + trimMultiline(decision.implementationOption) + "\n";
|
|
224
|
+
}
|
|
225
|
+
if (trimMultiline(decision.reason)) {
|
|
226
|
+
chunk += quotedReviewField("Reason", decision.reason);
|
|
227
|
+
}
|
|
228
|
+
return chunk;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildUserResponseMarkdown(runMeta, entries, createdAt, planDecision, analysisReview) {
|
|
196
232
|
var head =
|
|
197
233
|
"---\n" +
|
|
198
234
|
"task-key: " + (runMeta["task-key"] || "") + "\n" +
|
|
@@ -206,7 +242,7 @@
|
|
|
206
242
|
"# User Response\n";
|
|
207
243
|
|
|
208
244
|
entries = entries || [];
|
|
209
|
-
var
|
|
245
|
+
var hasPlanDecision = !!planDecision;
|
|
210
246
|
var hasAnalysisReview = !!analysisReview;
|
|
211
247
|
var chunks = "";
|
|
212
248
|
for (var i = 0; i < entries.length; i++) {
|
|
@@ -224,15 +260,11 @@
|
|
|
224
260
|
}
|
|
225
261
|
chunks += chunk;
|
|
226
262
|
}
|
|
227
|
-
if (entries.length === 0 && !
|
|
263
|
+
if (entries.length === 0 && !hasPlanDecision && !hasAnalysisReview) {
|
|
228
264
|
chunks += "\n_(No user responses recorded.)_\n";
|
|
229
265
|
}
|
|
230
|
-
if (
|
|
231
|
-
chunks +=
|
|
232
|
-
if (approval.implementationOption) {
|
|
233
|
-
chunks +=
|
|
234
|
-
"- Implementation-Option: " + trimMultiline(approval.implementationOption) + "\n";
|
|
235
|
-
}
|
|
266
|
+
if (hasPlanDecision) {
|
|
267
|
+
chunks += serialisePlanDecision(planDecision);
|
|
236
268
|
}
|
|
237
269
|
if (hasAnalysisReview) {
|
|
238
270
|
chunks += serialiseAnalysisReview(analysisReview);
|
|
@@ -268,10 +300,11 @@
|
|
|
268
300
|
function exportUserResponse() {
|
|
269
301
|
var runMeta = readRunMeta();
|
|
270
302
|
var entries = collectEntries();
|
|
271
|
-
var approval = collectApproval();
|
|
272
303
|
var out = document.getElementById("user-response-output");
|
|
304
|
+
var planDecision;
|
|
273
305
|
var analysisReview;
|
|
274
306
|
try {
|
|
307
|
+
planDecision = collectPlanDecision();
|
|
275
308
|
analysisReview = collectAnalysisReview();
|
|
276
309
|
} catch (e) {
|
|
277
310
|
showOutput(out, e.message || String(e));
|
|
@@ -279,13 +312,13 @@
|
|
|
279
312
|
return "";
|
|
280
313
|
}
|
|
281
314
|
var md = buildUserResponseMarkdown(
|
|
282
|
-
runMeta, entries, isoNowUtc(),
|
|
315
|
+
runMeta, entries, isoNowUtc(), planDecision, analysisReview
|
|
283
316
|
);
|
|
284
317
|
showOutput(out, md);
|
|
285
318
|
showDismiss(true);
|
|
286
319
|
// Nothing answered yet — show the empty serialisation as feedback but
|
|
287
320
|
// don't download a useless file.
|
|
288
|
-
if (entries.length > 0 ||
|
|
321
|
+
if (entries.length > 0 || planDecision || analysisReview) {
|
|
289
322
|
downloadUserResponse(md, runMeta);
|
|
290
323
|
}
|
|
291
324
|
return md;
|
|
@@ -415,7 +448,7 @@
|
|
|
415
448
|
window.okstraReportView = {
|
|
416
449
|
buildUserResponseMarkdown: buildUserResponseMarkdown,
|
|
417
450
|
collectEntries: collectEntries,
|
|
418
|
-
|
|
451
|
+
collectPlanDecision: collectPlanDecision,
|
|
419
452
|
collectAnalysisReview: collectAnalysisReview,
|
|
420
453
|
exportUserResponse: exportUserResponse,
|
|
421
454
|
setReaderMode: setReaderMode,
|
|
@@ -63,20 +63,24 @@ created-at: 2026-05-17T10:00:00Z
|
|
|
63
63
|
> (prediction=0: 1,204) (prediction=1: 38)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
##
|
|
66
|
+
## PLAN DECISION block (implementation-planning only)
|
|
67
67
|
|
|
68
|
-
When you
|
|
68
|
+
When you pick a verdict in the Plan Decision widget and Export, the following block is appended to the end of the body:
|
|
69
69
|
|
|
70
70
|
```markdown
|
|
71
|
-
##
|
|
72
|
-
-
|
|
71
|
+
## PLAN DECISION
|
|
72
|
+
- Status: <approved | revision-requested | rejected>
|
|
73
73
|
- Implementation-Option: <the name exactly as in Option Candidates>
|
|
74
|
+
- Reason:
|
|
75
|
+
> <one quoted line per input line>
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
78
|
+
- `revision-requested` and `rejected` require a reason: a plan sent back without one leaves the next run guessing at what to change, so Export refuses to serialise it.
|
|
79
|
+
- `Implementation-Option:` is emitted only for `approved`, and only when you moved off the recommended default — otherwise implementation falls back to the plan's Recommended Option.
|
|
80
|
+
- `Reason:` is optional for `approved`, and the line is omitted when empty.
|
|
81
|
+
- The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies it through the existing `--approve` / `--implementation-option` path; the sidecar itself does not bypass approval-gate validation. A non-approved status is ignored there — that step only ever looks for an approval.
|
|
82
|
+
- The parser is `parse_plan_decision` in `scripts/okstra_ctl/user_response.py`, and it accepts only the lowercase statuses that are byte-identical to the producer output (hand-edited values such as `Approved` are rejected fail-closed).
|
|
83
|
+
- `--resume-clarification` attaches the sidecar verbatim, so the PLAN DECISION block reaches the next planning run — which is how a rejection and its reason get acted on.
|
|
80
84
|
|
|
81
85
|
## ANALYSIS REVIEW block (analysis reports only)
|
|
82
86
|
|
|
@@ -29,7 +29,7 @@ When `**Evidence ledger:** required-v1` is present, append one canonical row to
|
|
|
29
29
|
|
|
30
30
|
- Evidence read: `<project-relative path without a line suffix>`
|
|
31
31
|
|
|
32
|
-
Every file citation in the result MUST use backticks
|
|
32
|
+
Every file citation in the result MUST use backticks, a line suffix, and the same project-relative path its ledger row carries — for example `src/config/env.ts:1-22`. A bare filename (`env.ts:1-22`) does not match its row and fails exactly like a file you never opened, however many times you cited the full path earlier. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
|
|
33
33
|
|
|
34
34
|
## Anchor headers (lead-injected, BLOCKING)
|
|
35
35
|
|
|
@@ -235,8 +235,14 @@ def _check_slice_tdd(text: str, stages: List[StageMapStage]) -> List[ValidationE
|
|
|
235
235
|
|
|
236
236
|
|
|
237
237
|
def _check_red_green_steps(section: str, stage_number: int) -> List[ValidationError]:
|
|
238
|
-
"""S10c: RED-first + later GREEN, with each step's `
|
|
239
|
-
its prefix (RED → FAIL, GREEN → PASS).
|
|
238
|
+
"""S10c: RED-first + later GREEN, with each step's `outcome` cell matching
|
|
239
|
+
its prefix (RED → FAIL, GREEN → PASS).
|
|
240
|
+
|
|
241
|
+
`outcome` is its own column, holding one of two words; the sentence about
|
|
242
|
+
what that looks like lives in `expected` beside it. Written as one cell the
|
|
243
|
+
author had to bury a literal token inside prose, and the rule fired on
|
|
244
|
+
fourteen rows of one measured plan whose prose was correct throughout.
|
|
245
|
+
"""
|
|
240
246
|
rows = _effective_step_rows(section)
|
|
241
247
|
steps = [(r[1], r[4]) for r in rows if len(r) > 4]
|
|
242
248
|
actions = [a for a, _ in steps]
|
|
@@ -248,16 +254,14 @@ def _check_red_green_steps(section: str, stage_number: int) -> List[ValidationEr
|
|
|
248
254
|
"S10c: first step action must start with 'RED:' and some "
|
|
249
255
|
"step action with 'GREEN:', or add a 'TDD exemption:' line"))
|
|
250
256
|
return errs
|
|
251
|
-
for action,
|
|
252
|
-
|
|
253
|
-
if action.startswith("RED:") and "FAIL"
|
|
257
|
+
for action, outcome in steps:
|
|
258
|
+
cell = outcome.strip().upper()
|
|
259
|
+
if action.startswith("RED:") and cell != "FAIL":
|
|
254
260
|
errs.append(ValidationError("S10", stage_number,
|
|
255
|
-
"S10c: 'RED:' step's
|
|
256
|
-
|
|
257
|
-
elif action.startswith("GREEN:") and "PASS" not in exp:
|
|
261
|
+
f"S10c: 'RED:' step's outcome cell must be FAIL, got '{outcome}'"))
|
|
262
|
+
elif action.startswith("GREEN:") and cell != "PASS":
|
|
258
263
|
errs.append(ValidationError("S10", stage_number,
|
|
259
|
-
"S10c: 'GREEN:' step's
|
|
260
|
-
f"'{expected}'"))
|
|
264
|
+
f"S10c: 'GREEN:' step's outcome cell must be PASS, got '{outcome}'"))
|
|
261
265
|
return errs
|
|
262
266
|
|
|
263
267
|
|
|
@@ -432,18 +436,9 @@ def _check_data_slice_tdd(stage: dict) -> List[ValidationError]:
|
|
|
432
436
|
return [ValidationError("S10", number,
|
|
433
437
|
"S10c: first stepwiseExecution action must start with 'RED:' and "
|
|
434
438
|
"some action with 'GREEN:', or declare a 'tddExemption'")]
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
action = str(step.get("action") or "")
|
|
439
|
-
expected = str(step.get("expected") or "")
|
|
440
|
-
if action.startswith("RED:") and "FAIL" not in expected.upper():
|
|
441
|
-
errs.append(ValidationError("S10", number,
|
|
442
|
-
f"S10c: 'RED:' step's expected must read FAIL, got '{expected}'"))
|
|
443
|
-
elif action.startswith("GREEN:") and "PASS" not in expected.upper():
|
|
444
|
-
errs.append(ValidationError("S10", number,
|
|
445
|
-
f"S10c: 'GREEN:' step's expected must read PASS, got '{expected}'"))
|
|
446
|
-
return errs
|
|
439
|
+
# Whether each row's own `outcome` agrees with its RED/GREEN prefix is a
|
|
440
|
+
# schema conditional now (`StageStepRow.allOf`), not a string search here.
|
|
441
|
+
return []
|
|
447
442
|
|
|
448
443
|
|
|
449
444
|
def _check_data_step_counts(
|
|
@@ -23,7 +23,6 @@ per line to stderr.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
|
-
import hashlib
|
|
27
26
|
import json
|
|
28
27
|
import re
|
|
29
28
|
import sys
|
|
@@ -40,9 +39,7 @@ from okstra_ctl.clarification_items import ( # noqa: E402
|
|
|
40
39
|
)
|
|
41
40
|
from okstra_ctl.report_views import ( # noqa: E402
|
|
42
41
|
analysis_review_context,
|
|
43
|
-
extract_html_digest,
|
|
44
42
|
plan_approval_context,
|
|
45
|
-
source_digest,
|
|
46
43
|
)
|
|
47
44
|
from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
|
|
48
45
|
from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
|
|
@@ -76,15 +73,6 @@ def _main_body(html_text: str) -> str:
|
|
|
76
73
|
return match.group("body")
|
|
77
74
|
|
|
78
75
|
|
|
79
|
-
def _sha256(path: Path) -> str:
|
|
80
|
-
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _digest_from_run_meta(html_text: str, key: str) -> str:
|
|
84
|
-
match = re.search(rf'"{re.escape(key)}"\s*:\s*"([0-9a-f]{{64}})"', html_text)
|
|
85
|
-
return match.group(1) if match else ""
|
|
86
|
-
|
|
87
|
-
|
|
88
76
|
def _load_v2_data(report_path: Path) -> tuple[Path, dict] | None:
|
|
89
77
|
data_path = final_report_data_path(report_path)
|
|
90
78
|
if not data_path.is_file():
|
|
@@ -106,17 +94,6 @@ def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
|
|
|
106
94
|
main_body = _main_body(html_text)
|
|
107
95
|
failures: list[str] = []
|
|
108
96
|
|
|
109
|
-
for key, path in (
|
|
110
|
-
("source-data-sha256", data_path),
|
|
111
|
-
("source-md-sha256", report_path),
|
|
112
|
-
):
|
|
113
|
-
recorded = _digest_from_run_meta(html_text, key)
|
|
114
|
-
expected = _sha256(path)
|
|
115
|
-
if not recorded:
|
|
116
|
-
failures.append(f"html missing {key} in run-meta")
|
|
117
|
-
elif recorded != expected:
|
|
118
|
-
failures.append(f"stale html: {key} does not match current source")
|
|
119
|
-
|
|
120
97
|
task_type = data.get("header", {}).get("taskType", "")
|
|
121
98
|
if f'data-task-template="{task_type}"' not in html_text:
|
|
122
99
|
failures.append(f"v2 html task template mismatch for {task_type}")
|
|
@@ -236,22 +213,6 @@ def validate(report_path: Path) -> list[str]:
|
|
|
236
213
|
html_text = html_path.read_text(encoding="utf-8")
|
|
237
214
|
html_body = _main_body(html_text)
|
|
238
215
|
|
|
239
|
-
# (2) source-digest staleness — html's run-meta source-sha256 must
|
|
240
|
-
# match the current MD's digest. Mismatch means the html was
|
|
241
|
-
# rendered against an older MD body and Phase 7 step 1.5 was not
|
|
242
|
-
# re-run.
|
|
243
|
-
expected_md_digest = source_digest(md)
|
|
244
|
-
html_digest = extract_html_digest(html_text)
|
|
245
|
-
if html_digest is None:
|
|
246
|
-
failures.append(
|
|
247
|
-
"html missing source-sha256 in run-meta — re-render with okstra render-views"
|
|
248
|
-
)
|
|
249
|
-
elif html_digest != expected_md_digest:
|
|
250
|
-
failures.append(
|
|
251
|
-
f"stale html: source-sha256 {html_digest[:12]}… does not match "
|
|
252
|
-
f"current MD {expected_md_digest[:12]}…"
|
|
253
|
-
)
|
|
254
|
-
|
|
255
216
|
# (3) deliverable sections contain no form controls
|
|
256
217
|
for chunk in _no_form_sections(html_body):
|
|
257
218
|
if "<textarea" in chunk or "<input" in chunk or "<select" in chunk:
|