okstra 0.209.6 → 0.211.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 (40) hide show
  1. package/docs/cli.md +2 -2
  2. package/package.json +1 -1
  3. package/runtime/BUILD.json +2 -2
  4. package/runtime/prompts/wizard/prompts.ko.json +7 -6
  5. package/runtime/python/okstra_ctl/analysis_packet.py +7 -3
  6. package/runtime/python/okstra_ctl/incremental_scope.py +11 -2
  7. package/runtime/python/okstra_ctl/manager_launch.py +2 -1
  8. package/runtime/python/okstra_ctl/manager_paths.py +4 -0
  9. package/runtime/python/okstra_ctl/phases/final_verification/target.py +5 -0
  10. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +5 -4
  11. package/runtime/python/okstra_ctl/phases/implementation/profile.md +1 -1
  12. package/runtime/python/okstra_ctl/phases/implementation/report.py +66 -19
  13. package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +8 -7
  14. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +1 -0
  15. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +8 -3
  16. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -2
  17. package/runtime/python/okstra_ctl/phases/implementation_planning/report_assets/implementation-planning-input.template.md +1 -1
  18. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +6 -0
  19. package/runtime/python/okstra_ctl/plan_items.py +24 -3
  20. package/runtime/python/okstra_ctl/report_html/run_usage.py +9 -0
  21. package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -8
  22. package/runtime/python/okstra_ctl/report_projections.py +18 -2
  23. package/runtime/python/okstra_ctl/report_synthesis_packet.py +19 -0
  24. package/runtime/python/okstra_ctl/report_translation.py +27 -6
  25. package/runtime/python/okstra_ctl/stage_close.py +13 -2
  26. package/runtime/python/okstra_ctl/stage_map.py +25 -0
  27. package/runtime/python/okstra_ctl/stage_map_cli.py +3 -2
  28. package/runtime/python/okstra_ctl/stage_targets.py +17 -3
  29. package/runtime/python/okstra_ctl/wizard/sources.py +2 -1
  30. package/runtime/python/okstra_ctl/wizard/state.py +30 -1
  31. package/runtime/python/okstra_ctl/wizard/steps_options.py +19 -0
  32. package/runtime/python/okstra_ctl/wizard/steps_plan.py +4 -2
  33. package/runtime/python/okstra_ctl/wizard/steps_roles.py +7 -1
  34. package/runtime/schemas/final-report-v3.0.schema.json +18 -0
  35. package/runtime/templates/reports/html/assets/base.css +4 -2
  36. package/runtime/templates/reports/html/base.template.html +14 -9
  37. package/runtime/templates/reports/html/i18n/en.json +91 -2
  38. package/runtime/templates/reports/html/i18n/ko.json +91 -2
  39. package/runtime/templates/reports/html/macros/forms.html +8 -7
  40. package/runtime/validators/validate-implementation-plan-stages.py +46 -5
@@ -147,6 +147,74 @@
147
147
  "deferred": "Deferred",
148
148
  "not-applicable": "Not applicable",
149
149
  "blocked": "Blocked"
150
+ },
151
+ "clarificationKind": {
152
+ "material": "Missing information",
153
+ "decision": "Your decision",
154
+ "data-point": "A fact to confirm"
155
+ },
156
+ "clarificationStatus": {
157
+ "open": "Open",
158
+ "answered": "Answered",
159
+ "resolved": "Resolved",
160
+ "obsolete": "No longer relevant"
161
+ },
162
+ "approvalDisposition": {
163
+ "select": "Choose this option",
164
+ "accept-risk": "Accept the risk",
165
+ "request-revision": "Request a revision",
166
+ "reject": "Reject"
167
+ },
168
+ "approvalClassification": {
169
+ "user-decision": "Needs your decision",
170
+ "noncritical-dissent": "Non-critical disagreement",
171
+ "correctness-critical": "Affects correctness"
172
+ },
173
+ "clarificationScope": {
174
+ "in-repo": "This repository only",
175
+ "cross-repo": "Other repositories too",
176
+ "new-schema": "New schema",
177
+ "deferrable": "Can be deferred"
178
+ },
179
+ "executionRole": {
180
+ "leader": "Lead",
181
+ "analyser": "Analyser",
182
+ "critic": "Critic",
183
+ "designer": "Designer",
184
+ "implementer": "Implementer",
185
+ "planner": "Planner",
186
+ "report-writer": "Report writer",
187
+ "translator": "Translator",
188
+ "verifier": "Verifier"
189
+ },
190
+ "requirementStatus": {
191
+ "covered": "Covered",
192
+ "gap": "Not covered",
193
+ "documented-deviation": "Approved deviation",
194
+ "blocked": "Blocked"
195
+ },
196
+ "rollbackCategory": {
197
+ "Pure code": "Code only",
198
+ "Feature-flag-gated": "Behind a feature flag",
199
+ "Schema/config/stateful": "Schema, config or stored state"
200
+ },
201
+ "rollbackResult": {
202
+ "ok": "Passed",
203
+ "not-applicable": "Not applicable",
204
+ "advisory — human-run": "Advisory: a person runs it"
205
+ },
206
+ "businessFlowMode": {
207
+ "pre": "Before the change",
208
+ "contribute": "This phase's contribution",
209
+ "post": "After the change",
210
+ "unknown": "Unknown phase"
211
+ },
212
+ "businessFlowStatus": {
213
+ "prepared": "Prepared",
214
+ "running": "Running",
215
+ "failed": "Failed",
216
+ "partial": "Partly explained",
217
+ "complete": "Explained"
150
218
  }
151
219
  },
152
220
  "enumHint": {
@@ -194,7 +262,12 @@
194
262
  "agent-activity": "Agent activity",
195
263
  "agent-activity-intro": "What each agent was asked to do in this run and how it ended. Decision cards and prose cite these rows by id.",
196
264
  "agent": "Agent",
197
- "activity": "Activity"
265
+ "activity": "Activity",
266
+ "business-flow": "Business flow explanations",
267
+ "business-flow-failed-count": "{count} business flow explanations failed",
268
+ "business-flow-failure-details": "Failure details and rerun commands",
269
+ "business-flow-error": "Error:",
270
+ "business-flow-rerun": "Rerun:"
198
271
  },
199
272
  "runUsage": {
200
273
  "heading": "What this run cost",
@@ -214,7 +287,8 @@
214
287
  "row-total": "Total",
215
288
  "row-cli": "CLI calls (billed separately)",
216
289
  "cache-read-tokens": "Cache re-reads",
217
- "row-task-cumulative": "This task so far ({runs} runs)"
290
+ "row-task-cumulative": "This task so far ({runs} runs)",
291
+ "row-total-with-cli": "Total including CLI calls"
218
292
  },
219
293
  "macros": {
220
294
  "forms": {
@@ -585,6 +659,21 @@
585
659
  "result": "Result",
586
660
  "no-rollback-check-was-recorded": "No rollback check was recorded.",
587
661
  "stage-carry-over": "Stage carry-over",
662
+ "carry-sidecar": "Sidecar",
663
+ "carry-commit-range": "Commit range",
664
+ "carry-steps": "Steps",
665
+ "carry-files-changed": "Files changed",
666
+ "carry-raw": "Sidecar JSON as written",
667
+ "file": "File",
668
+ "change": "Change",
669
+ "lines": "Lines",
670
+ "plan-step": "Plan step",
671
+ "action-created": "Created",
672
+ "action-modified": "Modified",
673
+ "action-deleted": "Deleted",
674
+ "what-it-does": "What it does",
675
+ "advisory": "advisory",
676
+ "entry-count": "{count} entries",
588
677
  "manual-user-test": "Manual user test",
589
678
  "environment": "Environment:",
590
679
  "expected": "Expected:",
@@ -147,6 +147,74 @@
147
147
  "deferred": "보류",
148
148
  "not-applicable": "해당 없음",
149
149
  "blocked": "차단"
150
+ },
151
+ "clarificationKind": {
152
+ "material": "빠진 정보",
153
+ "decision": "결정 필요",
154
+ "data-point": "확인할 사실"
155
+ },
156
+ "clarificationStatus": {
157
+ "open": "답변 대기",
158
+ "answered": "답변함",
159
+ "resolved": "해결됨",
160
+ "obsolete": "더 이상 해당 없음"
161
+ },
162
+ "approvalDisposition": {
163
+ "select": "이 선택지 채택",
164
+ "accept-risk": "위험 감수",
165
+ "request-revision": "수정 요청",
166
+ "reject": "반려"
167
+ },
168
+ "approvalClassification": {
169
+ "user-decision": "사용자 결정 필요",
170
+ "noncritical-dissent": "치명적이지 않은 이견",
171
+ "correctness-critical": "정확성에 영향"
172
+ },
173
+ "clarificationScope": {
174
+ "in-repo": "이 저장소 안",
175
+ "cross-repo": "다른 저장소까지",
176
+ "new-schema": "새 스키마",
177
+ "deferrable": "미룰 수 있음"
178
+ },
179
+ "executionRole": {
180
+ "leader": "리드",
181
+ "analyser": "분석",
182
+ "critic": "비평",
183
+ "designer": "설계",
184
+ "implementer": "구현",
185
+ "planner": "계획",
186
+ "report-writer": "리포트 작성",
187
+ "translator": "번역",
188
+ "verifier": "검증"
189
+ },
190
+ "requirementStatus": {
191
+ "covered": "충족",
192
+ "gap": "미충족",
193
+ "documented-deviation": "승인된 이탈",
194
+ "blocked": "차단"
195
+ },
196
+ "rollbackCategory": {
197
+ "Pure code": "코드만",
198
+ "Feature-flag-gated": "기능 플래그로 제어",
199
+ "Schema/config/stateful": "스키마·설정·저장 상태"
200
+ },
201
+ "rollbackResult": {
202
+ "ok": "통과",
203
+ "not-applicable": "해당 없음",
204
+ "advisory — human-run": "권고: 사람이 직접 실행"
205
+ },
206
+ "businessFlowMode": {
207
+ "pre": "변경 전",
208
+ "contribute": "이번 단계 기여",
209
+ "post": "변경 후",
210
+ "unknown": "알 수 없는 단계"
211
+ },
212
+ "businessFlowStatus": {
213
+ "prepared": "준비됨",
214
+ "running": "실행 중",
215
+ "failed": "실패",
216
+ "partial": "일부 설명",
217
+ "complete": "설명 완료"
150
218
  }
151
219
  },
152
220
  "enumHint": {
@@ -194,7 +262,12 @@
194
262
  "agent-activity": "에이전트 활동",
195
263
  "agent-activity-intro": "이번 run 에서 각 에이전트가 맡은 일과 그 결과입니다. 결정 카드와 본문이 이 행을 아이디로 인용합니다.",
196
264
  "agent": "에이전트",
197
- "activity": "활동"
265
+ "activity": "활동",
266
+ "business-flow": "업무 흐름 설명",
267
+ "business-flow-failed-count": "업무 흐름 설명 생성 실패 {count}건",
268
+ "business-flow-failure-details": "실패 내역과 재실행 명령",
269
+ "business-flow-error": "오류:",
270
+ "business-flow-rerun": "재실행:"
198
271
  },
199
272
  "runUsage": {
200
273
  "heading": "이번 실행에 든 비용",
@@ -214,7 +287,8 @@
214
287
  "row-total": "합계",
215
288
  "row-cli": "CLI 호출 (별도 청구)",
216
289
  "cache-read-tokens": "캐시 재읽기",
217
- "row-task-cumulative": "이 태스크 누적 (run {runs}회)"
290
+ "row-task-cumulative": "이 태스크 누적 (run {runs}회)",
291
+ "row-total-with-cli": "CLI 호출 포함 합계"
218
292
  },
219
293
  "macros": {
220
294
  "forms": {
@@ -585,6 +659,21 @@
585
659
  "result": "결과",
586
660
  "no-rollback-check-was-recorded": "롤백 확인 기록이 없습니다.",
587
661
  "stage-carry-over": "stage 인계",
662
+ "carry-sidecar": "인계 파일",
663
+ "carry-commit-range": "commit 범위",
664
+ "carry-steps": "단계",
665
+ "carry-files-changed": "바뀐 파일 수",
666
+ "carry-raw": "인계 JSON 원문",
667
+ "file": "파일",
668
+ "change": "변경",
669
+ "lines": "줄 수",
670
+ "plan-step": "계획 단계",
671
+ "action-created": "추가",
672
+ "action-modified": "수정",
673
+ "action-deleted": "삭제",
674
+ "what-it-does": "하는 일",
675
+ "advisory": "권고",
676
+ "entry-count": "{count}개 항목",
588
677
  "manual-user-test": "수동 사용자 테스트",
589
678
  "environment": "환경:",
590
679
  "expected": "기대 결과:",
@@ -57,18 +57,18 @@
57
57
  {%- endmacro %}
58
58
 
59
59
  {% macro clarification_article(row, compact=false) -%}
60
- {% set is_closed = row.status in ['resolved', 'obsolete'] %}
60
+ {% set is_closed = compact or row.status in ['resolved', 'obsolete'] %}
61
61
  {% set approval_context = row.approvalContext | default(None) %}
62
62
  {% set resolution = row.resolution | default(None) %}
63
63
  {% set options = row.options | default([]) %}
64
64
  <article class="clarification-item{% if compact %} is-closed{% endif %}" id="id-{{ row.id }}" data-response-id="{{ row.id }}" data-kind="{{ row.kind }}" data-status="{{ row.status }}">
65
- {% if compact %}<details><summary>{{ row.id | inline_code }} · {{ row.status }}</summary>{% endif %}
66
- <p class="eyebrow">{{ row.id | inline_code }} · {{ row.kind }}</p>
65
+ {% if compact %}<details><summary>{{ row.id | inline_code }} · {{ row.status | enum_label('clarificationStatus') }}</summary>{% endif %}
66
+ <p class="eyebrow">{{ row.id | inline_code }} · {{ row.kind | enum_label('clarificationKind') }}</p>
67
67
  {{ row.statement | paragraphs }}
68
68
  <dl class="clarification-expected"><dt>{{ t('macros.forms.answer-as') }}</dt><dd>{{ row.expectedForm | inline_code }}</dd></dl>
69
69
  {% if approval_context %}
70
70
  <div class="approval-context" data-approval-classification="{{ approval_context.classification }}">
71
- <p class="eyebrow">{{ approval_context.classification }}</p>
71
+ <p class="eyebrow">{{ approval_context.classification | enum_label('approvalClassification') }}</p>
72
72
  <p><strong>{{ t('tasks.implementation-planning.unblock-condition') }}</strong> {{ approval_context.unblockCondition | inline_code }}</p>
73
73
  {% set check_refs = resolution.checkRefs if resolution else approval_context.activityIds | default([]) %}
74
74
  {% if check_refs %}<p><strong>{{ t('tasks.implementation-planning.agent-evidence') }}</strong>
@@ -81,10 +81,10 @@
81
81
  {% for option in options %}
82
82
  <li class="clarification-option{% if option.role == 'recommended' %} is-recommended{% endif %}" data-option-value="{{ option.answer }}">
83
83
  <p class="clarification-option-answer">{{ option.answer | inline_code }}{% if option.role == 'recommended' %} <span class="badge">{{ t('macros.forms.recommended') }}</span>{% endif %}</p>
84
- {% if option.disposition | default(None) %}<p class="clarification-option-disposition"><code>{{ option.disposition }}</code></p>{% endif %}
84
+ {% if option.disposition | default(None) %}<p class="clarification-option-disposition">{{ option.disposition | enum_label('approvalDisposition') }}</p>{% endif %}
85
85
  <p class="clarification-option-rationale">{{ option.rationale | inline_code }}</p>
86
86
  <dl class="clarification-option-impact">
87
- <dt>{{ t('macros.forms.scope-impact') }}</dt><dd>{% if option.reach | default(None) %}{{ option.reach }}{% if option.scopeEffects | default([]) %}, {{ option.scopeEffects | join(', ') }}{% endif %}{% else %}{{ option.scopeImpact | join(', ') }}{% endif %}</dd>
87
+ <dt>{{ t('macros.forms.scope-impact') }}</dt><dd>{% if option.reach | default(None) %}{{ option.reach | enum_label('clarificationScope') }}{% if option.scopeEffects | default([]) %}, {{ option.scopeEffects | map('enum_label', 'clarificationScope') | join(', ') }}{% endif %}{% else %}{{ option.scopeImpact | map('enum_label', 'clarificationScope') | join(', ') }}{% endif %}</dd>
88
88
  <dt>{{ t('macros.forms.added-work') }}</dt><dd>{{ option.addedWork | inline_code }}</dd>
89
89
  <dt>{{ t('macros.forms.direction-change') }}</dt><dd>{{ option.directionChange | inline_code }}</dd>
90
90
  </dl>
@@ -114,7 +114,8 @@
114
114
  {% if items %}
115
115
  {% set ns = namespace(open=[], closed=[]) %}
116
116
  {% for row in items %}
117
- {% if row.status in ['open', 'answered'] %}
117
+ {# 답이 실린 answered 행은 기다리는 질문이 아니다 — 닫힌 목록으로 간다. #}
118
+ {% if row.status == 'open' or (row.status == 'answered' and not row.userInput | default('')) %}
118
119
  {% set ns.open = ns.open + [row] %}
119
120
  {% else %}
120
121
  {% set ns.closed = ns.closed + [row] %}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """S1–S13 checks for the Stage Map structure of an approved
2
+ """S1–S14 checks for the Stage Map structure of an approved
3
3
  implementation-planning final-report.md. Run from prepare_task_bundle
4
4
  of `implementation` task or standalone."""
5
5
 
@@ -35,6 +35,7 @@ from okstra_ctl.stage_map import ( # noqa: E402
35
35
  parse_stage_map_text,
36
36
  schema_v2_report,
37
37
  stage_number_gap_message,
38
+ stage_row_is_cancelled,
38
39
  )
39
40
 
40
41
  HARD_STEP_CAP = 8
@@ -68,9 +69,14 @@ BARE_GIT_STATUS = re.compile(r"\bgit\b[^&|;]*?\bstatus\b[^&|;]*?--(?:porcelain|s
68
69
  CLEAN_GATE_COMMAND = "okstra worktree-status --check-clean"
69
70
 
70
71
 
72
+ # S14 — 취소 stage 가 될 수 없는 상태. 이미 착수했거나 끝난 stage 는 통합 기준선의
73
+ # 일부라 취소하면 그 위에 쌓인 브랜치와 갈라진다(ADR-0015); 고치려면 max+1 보정 stage.
74
+ _UNCANCELLABLE_STATUSES = ("done", "active")
75
+
76
+
71
77
  @dataclass
72
78
  class ValidationError:
73
- code: str # S1..S13
79
+ code: str # S1..S14
74
80
  stage: int # 0 = global
75
81
  message: str
76
82
 
@@ -380,13 +386,19 @@ def _check_conformance_declaration(
380
386
  return errs
381
387
 
382
388
 
383
- def _check_depends_on(stages: List[StageMapStage]) -> List[ValidationError]:
389
+ def _check_depends_on(
390
+ stages: List[StageMapStage], cancelled: frozenset[int] = frozenset(),
391
+ ) -> List[ValidationError]:
384
392
  errs: List[ValidationError] = []
385
393
  valid = {s.stage_number for s in stages}
386
394
  for s in stages:
387
395
  for d in s.depends_on:
388
396
  if d == s.stage_number:
389
397
  errs.append(ValidationError("S8", s.stage_number, "self depends-on"))
398
+ elif d in cancelled:
399
+ errs.append(ValidationError("S14", s.stage_number,
400
+ f"depends-on {d}, which this plan cancels — a cancelled stage "
401
+ "is never executed; depend on the stage that replaces it"))
390
402
  elif d not in valid:
391
403
  errs.append(ValidationError("S6", s.stage_number,
392
404
  f"depends-on {d} does not exist"))
@@ -495,6 +507,7 @@ def _data_stage_metas(
495
507
  depends = parse_stage_dependencies(
496
508
  str(row.get("dependsOn") or ""), row_number, ""
497
509
  )
510
+ cancelled = stage_row_is_cancelled(row, row_number)
498
511
  except StageMapError as exc:
499
512
  errors.append(ValidationError("S2", row["stage"], exc.reason))
500
513
  continue
@@ -504,6 +517,7 @@ def _data_stage_metas(
504
517
  depends,
505
518
  row.get("stepCount") if isinstance(row.get("stepCount"), int) else -1,
506
519
  str(row.get("exitContractSummary") or ""),
520
+ cancelled,
507
521
  ))
508
522
  return rows, errors or _stage_numbers_monotonic(rows)
509
523
 
@@ -670,6 +684,28 @@ def _check_data_stage_identities(
670
684
  return errs
671
685
 
672
686
 
687
+ def _check_cancelled_stage_ledger(
688
+ stage_map: List[StageMapStage], planning: dict,
689
+ ) -> List[ValidationError]:
690
+ """S14: 원장이 이미 착수했거나 끝났다고 기록한 stage 는 취소할 수 없다."""
691
+ verification = planning.get("planBodyVerification")
692
+ ledger = (
693
+ verification.get("stageLedger") if isinstance(verification, dict) else None
694
+ )
695
+ if not isinstance(ledger, dict):
696
+ return []
697
+ return [
698
+ ValidationError("S14", meta.stage_number,
699
+ f"stage {meta.stage_number} is cancelled but the Stage Ledger records "
700
+ f"it {ledger[str(meta.stage_number)]!r}; only a ready or blocked stage "
701
+ "may be cancelled — keep it and add a correction stage numbered after "
702
+ "the highest stage instead")
703
+ for meta in stage_map
704
+ if meta.cancelled
705
+ and ledger.get(str(meta.stage_number)) in _UNCANCELLABLE_STATUSES
706
+ ]
707
+
708
+
673
709
  def collect_data_validation_errors(
674
710
  planning: dict, user_bypassed: frozenset[int] = frozenset(),
675
711
  ) -> List[ValidationError]:
@@ -703,14 +739,19 @@ def collect_data_validation_errors(
703
739
  if identity_errors:
704
740
  return errors
705
741
  errors.extend(_check_data_step_counts(stage_map, stages))
706
- errors.extend(_check_depends_on(stage_map))
742
+ # 번호(S2)·대응(S3)·스텝 수는 모든 행에, 의존(S6/S8)·병렬 안전(S9)은 실행될
743
+ # 행에만 건다 — 취소 stage 와 그 대체 stage 는 같은 파일을 예측하기 마련이다.
744
+ active = [meta for meta in stage_map if not meta.cancelled]
745
+ cancelled = frozenset(meta.stage_number for meta in stage_map if meta.cancelled)
746
+ errors.extend(_check_depends_on(active, cancelled))
747
+ errors.extend(_check_cancelled_stage_ledger(stage_map, planning))
707
748
  errors.extend(_report_shared_parallel_files({
708
749
  meta.stage_number: set(PATH_TOKEN.findall(
709
750
  str((next(
710
751
  (s for s in stages if s.get("stage") == meta.stage_number), {}
711
752
  )).get("exitContract") or "")
712
753
  ))
713
- for meta in stage_map
754
+ for meta in active
714
755
  if not meta.depends_on
715
756
  }))
716
757
  for stage in stages: