okstra 0.34.0 → 0.36.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 (101) hide show
  1. package/README.kr.md +26 -16
  2. package/README.md +26 -16
  3. package/docs/kr/architecture.md +59 -45
  4. package/docs/kr/cli.md +61 -18
  5. package/docs/pr-template-usage.md +65 -0
  6. package/docs/project-structure-overview.md +358 -354
  7. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +1 -1
  8. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1 -1
  9. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +1 -1
  10. package/docs/superpowers/plans/2026-05-20-final-report-language.md +1501 -0
  11. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +1267 -0
  12. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +1007 -0
  13. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +720 -0
  14. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +681 -0
  15. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +1691 -0
  16. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +383 -0
  17. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +320 -0
  18. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +299 -0
  19. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +335 -0
  20. package/docs/task-process/README.md +74 -0
  21. package/docs/task-process/common-flow.md +166 -0
  22. package/docs/task-process/error-analysis.md +101 -0
  23. package/docs/task-process/final-verification.md +167 -0
  24. package/docs/task-process/implementation-planning.md +128 -0
  25. package/docs/task-process/implementation.md +149 -0
  26. package/docs/task-process/release-handoff.md +206 -0
  27. package/docs/task-process/requirements-discovery.md +115 -0
  28. package/package.json +1 -1
  29. package/runtime/BUILD.json +2 -2
  30. package/runtime/agents/SKILL.md +29 -13
  31. package/runtime/agents/workers/claude-worker.md +26 -0
  32. package/runtime/agents/workers/codex-worker.md +27 -1
  33. package/runtime/agents/workers/gemini-worker.md +27 -1
  34. package/runtime/agents/workers/report-writer-worker.md +8 -1
  35. package/runtime/bin/okstra-central.sh +6 -6
  36. package/runtime/bin/okstra-codex-exec.sh +49 -28
  37. package/runtime/bin/okstra-gemini-exec.sh +39 -21
  38. package/runtime/bin/okstra-render-final-report.py +13 -2
  39. package/runtime/bin/okstra-wrapper-status.py +155 -0
  40. package/runtime/bin/okstra.sh +2 -2
  41. package/runtime/prompts/profiles/_common-contract.md +11 -6
  42. package/runtime/prompts/profiles/error-analysis.md +3 -7
  43. package/runtime/prompts/profiles/implementation-planning.md +22 -21
  44. package/runtime/prompts/profiles/implementation.md +28 -11
  45. package/runtime/prompts/profiles/improvement-discovery.md +42 -0
  46. package/runtime/prompts/profiles/kr/_common-contract.md +92 -0
  47. package/runtime/prompts/profiles/kr/error-analysis.md +36 -0
  48. package/runtime/prompts/profiles/kr/final-verification.md +48 -0
  49. package/runtime/prompts/profiles/kr/implementation-planning.md +90 -0
  50. package/runtime/prompts/profiles/kr/implementation.md +144 -0
  51. package/runtime/prompts/profiles/kr/improvement-discovery.md +42 -0
  52. package/runtime/prompts/profiles/kr/release-handoff.md +104 -0
  53. package/runtime/prompts/profiles/kr/requirements-discovery.md +42 -0
  54. package/runtime/prompts/profiles/release-handoff.md +1 -1
  55. package/runtime/prompts/profiles/requirements-discovery.md +8 -12
  56. package/runtime/prompts/wizard/prompts.ko.json +230 -0
  57. package/runtime/python/lib/okstra/cli.sh +2 -49
  58. package/runtime/python/lib/okstra/globals.sh +21 -21
  59. package/runtime/python/lib/okstra/interactive.sh +7 -7
  60. package/runtime/python/okstra_ctl/clarification_items.py +3 -9
  61. package/runtime/python/okstra_ctl/consumers.py +53 -0
  62. package/runtime/python/okstra_ctl/final_report_schema.py +0 -7
  63. package/runtime/python/okstra_ctl/i18n.py +73 -0
  64. package/runtime/python/okstra_ctl/improvement_lenses.py +44 -0
  65. package/runtime/python/okstra_ctl/index.py +1 -1
  66. package/runtime/python/okstra_ctl/paths.py +23 -20
  67. package/runtime/python/okstra_ctl/render.py +147 -202
  68. package/runtime/python/okstra_ctl/render_final_report.py +53 -10
  69. package/runtime/python/okstra_ctl/run.py +292 -107
  70. package/runtime/python/okstra_ctl/run_context.py +22 -0
  71. package/runtime/python/okstra_ctl/seeding.py +186 -0
  72. package/runtime/python/okstra_ctl/wizard.py +348 -127
  73. package/runtime/python/okstra_ctl/workflow.py +21 -2
  74. package/runtime/python/okstra_ctl/worktree.py +54 -1
  75. package/runtime/python/okstra_project/resolver.py +4 -3
  76. package/runtime/python/okstra_token_usage/report.py +2 -2
  77. package/runtime/schemas/final-report-v1.0.schema.json +22 -16
  78. package/runtime/skills/okstra-brief/SKILL.md +124 -31
  79. package/runtime/skills/okstra-convergence/SKILL.md +2 -3
  80. package/runtime/skills/okstra-report-writer/SKILL.md +35 -15
  81. package/runtime/skills/okstra-run/SKILL.md +5 -4
  82. package/runtime/skills/okstra-schedule/SKILL.md +4 -4
  83. package/runtime/skills/okstra-setup/SKILL.md +27 -0
  84. package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
  85. package/runtime/templates/okstra.CLAUDE.md +104 -0
  86. package/runtime/templates/reports/final-report.template.md +93 -98
  87. package/runtime/templates/reports/i18n/en.json +135 -0
  88. package/runtime/templates/reports/i18n/ko.json +135 -0
  89. package/runtime/templates/reports/implementation-planning-input.template.md +18 -0
  90. package/runtime/templates/reports/improvement-discovery-input.template.md +78 -0
  91. package/runtime/templates/reports/task-brief.template.md +2 -2
  92. package/runtime/validators/lib/fixtures.sh +30 -0
  93. package/runtime/validators/lib/runners.sh +1 -1
  94. package/runtime/validators/validate-implementation-plan-stages.py +211 -0
  95. package/runtime/validators/validate-run.py +121 -26
  96. package/runtime/validators/validate-workflow.sh +2 -2
  97. package/runtime/validators/validate_improvement_report.py +275 -0
  98. package/src/config.mjs +18 -0
  99. package/src/install.mjs +41 -14
  100. package/src/setup.mjs +133 -1
  101. package/src/uninstall.mjs +21 -1
@@ -0,0 +1,135 @@
1
+ {
2
+ "_meta": {
3
+ "lang": "ko",
4
+ "note": "okstra final-report 고정 문자열 (한국어). 키는 en.json 과 정확히 일치해야 한다. 키를 추가하면 en.json 도 같은 커밋에서 추가한다."
5
+ },
6
+ "emptyState": {
7
+ "consensusItems": "- 합의 항목 없음.",
8
+ "differences": "- 유의미한 차이 없음. 1.1 Consensus 가 그대로 유효합니다.",
9
+ "primaryEvidence": "- 주 증거 없음.",
10
+ "secondaryEvidence": "- 보조 증거 또는 대안 해석 없음.",
11
+ "risks": "- 누락된 정보·위험 없음.",
12
+ "dependencyRisk": "- 의존성·마이그레이션 위험 없음.",
13
+ "dissent": "- 반대 의견 없음.",
14
+ "outOfPlanEdits": "- 계획 외 편집 없음.",
15
+ "declinedFixRecommendations": "- 없음.",
16
+ "discrepancy": "- 없음.",
17
+ "lingeringRisks": "- 추적 대상 잔존 위험 없음.",
18
+ "noClarification": "- 추가 정보 요청 없음. Section 2 의 최종 판단이 그대로 유효합니다.",
19
+ "noFollowUp": "- 후속 작업 없음. 본 run 의 다음 phase 는 §6 (Recommended Next Steps) 참고."
20
+ },
21
+ "columns": {
22
+ "summary": "한 줄 요약",
23
+ "source": "출처 (brief/source/worker)",
24
+ "rawTokens": "처리 토큰",
25
+ "billableTokens": "환산 토큰",
26
+ "billableTokensInputEquiv": "환산 토큰 (input 기준)",
27
+ "cost": "비용 (USD)",
28
+ "checkMethod": "확인 방법"
29
+ },
30
+ "sectionAside": {
31
+ "dependencyRisk": "의존성·마이그레이션 위험",
32
+ "validationChecklist": "검증 체크리스트",
33
+ "rollbackStrategy": "롤백 전략",
34
+ "planBodyVerification": "계획 본문 검증",
35
+ "recommendedOption": "권장 옵션",
36
+ "optionCandidates": "옵션 후보",
37
+ "tradeOffMatrix": "트레이드오프 매트릭스",
38
+ "stepwiseExecutionOrder": "단계별 실행 순서"
39
+ },
40
+ "sectionIntro": {
41
+ "verdictCard": "한눈에 보는 결과 카드. 본 표의 모든 값은 `## 2. Final Verdict` 및 `## 6. Recommended Next Steps` 의 권위 있는 값과 정확히 일치해야 합니다.",
42
+ "clarificationCarryIn": "이전 보고서의 `## 5. Clarification Items` 표 매 행(`C-001`, `C-002`, …) 을 새 증거에 비추어 검토하고, 각 행의 `Status` 를 `resolved` 또는 `obsolete` 로 갱신한 뒤 본 run 의 `## 5.` 표에 carry-in 합니다. 해소 근거(파일:라인 / 로그 / 워커 결과) 를 함께 인용합니다.",
43
+ "ticketCoverage": "3~5 개 row 로 핵심 문제·요구사항·검증 대상을 표로 정리합니다. brief, 소스 자료, worker 결과를 근거로 작성합니다.",
44
+ "executionStatus": "각 worker 의 status, 배정 모델, key finding 을 한 표에 모읍니다. worker 산출물을 근거 없는 주장으로 대체하지 않습니다.",
45
+ "sourceItemsRule": "`Source items` 규칙: 본 합의 row 가 어느 워커의 어느 항목들에서 합성됐는지를 `<worker>:<item-id>` 페어 콤마-리스트로 적습니다. 자세한 정책은 `prompts/profiles/_common-contract.md` \"Cross-worker traceability\" SSOT.",
46
+ "stepRule": "규칙: 한 step 은 약 2~5 분. 모든 step 은 정확한 파일 경로와 명령어 포함.",
47
+ "planBodyVerification": "Phase 6 에서 report-writer 가 합성한 4.5 본문을 lead 가 plan-item 단위로 워커들에게 다시 던지고 평결을 수집한 결과.",
48
+ "clarificationItems": "다음 run 으로 넘어가기 전에 사용자가 답하거나 자료를 첨부해야 하는 항목을 **한 표 안에서** 추적합니다."
49
+ },
50
+ "tokenSummary": {
51
+ "heading": "토큰 사용량 요약",
52
+ "tableHeaderItem": "항목",
53
+ "rowLead": "Lead",
54
+ "rowWorkerTotal": "Worker 합계",
55
+ "rowGrandTotal": "**전체 합계**",
56
+ "rowCliExtra": "Codex/Gemini CLI 추가 비용"
57
+ },
58
+ "verdictCard": {
59
+ "tableHeaderLabel": "항목",
60
+ "tableHeaderValue": "값",
61
+ "approvalRequiredSuffix": "frontmatter `approved` 가 `true` 여야 `implementation` 진입 가능",
62
+ "rationaleLabel": "근거 요약",
63
+ "nextStepLabel": "다음 단계"
64
+ },
65
+ "ticketCoverage": {
66
+ "intro": "본 run 이 다룬 ticket 의 역방향 인덱스. 본문 항목들은 모두 `Ticket ID` 컬럼 또는 `[TICKETID: <id>]` 태그로 ticket 과 묶여 있습니다.",
67
+ "columnSections": "등장 섹션",
68
+ "columnRelatedIds": "관련 항목 IDs",
69
+ "ruleNote": "규칙: `Ticket ID` 는 본문에서 등장한 ticket 키와 정확히 동일 문자열. `Issue / Ticket` 이 비어 폴백된 경우 `Task ID` 값을 prefix 없이 그대로 (예: `8852`). 식별 불가는 `unknown`."
70
+ },
71
+ "finalVerdict": {
72
+ "intro": "최종 결론과 권장 방향을 한 표로 명시합니다. `Direction` ∈ `continue-investigation / begin-implementation / approve / reject / hold`. `task-type` 이 `final-verification` 이면 `Verdict Token` 은 `accepted / conditional-accept / blocked` 중 하나여야 하며, `release-handoff` 는 이 값을 진입 게이트로 사용합니다. 다른 task-type 에서는 `not-applicable`."
73
+ },
74
+ "evidence": {
75
+ "sourceItemsColumnNote": "`Source items` 컬럼 규칙은 §1.1 과 동일."
76
+ },
77
+ "roundHistory": {
78
+ "round2SkippedReasonNote": "값은 `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only` 중 하나."
79
+ },
80
+ "implementationPlanning": {
81
+ "optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
82
+ "optionBlastRadiusLabel": "폭발 반경 추정",
83
+ "recommendedTableHeaderLabel": "항목",
84
+ "recommendedTableHeaderValue": "값",
85
+ "coreReasonLabel": "핵심 이유",
86
+ "rationaleLabel": "근거 (Trade-off 행 / 원칙)",
87
+ "rejectedSummaryLabel": "채택되지 않은 옵션 요약",
88
+ "columnImpact": "영향",
89
+ "columnMitigation": "완화 / 선행 작업"
90
+ },
91
+ "releaseHandoff": {
92
+ "auditNote": "git/gh mutating 명령이 실행된 phase 의 감사 기록.",
93
+ "branchStateAside": "run 시작 시점",
94
+ "gitStatusShortLabel": "`git status --short` 출력",
95
+ "existingPrLabel": "기존 PR 존재 여부",
96
+ "userSelectionsAside": "메뉴 응답 기록",
97
+ "questionsTableHeader": {
98
+ "questionId": "질문 ID",
99
+ "questionBody": "질문 본문",
100
+ "userResponse": "사용자 응답 (원문)",
101
+ "allowedOptions": "응답이 가능한 보기"
102
+ },
103
+ "h1Body": "어떤 작업을 실행할까요?",
104
+ "h2Body": "PR base 브랜치 (H1=`push + PR` 인 경우)",
105
+ "h3Body": "PR title/body 초안 처리",
106
+ "h2DefaultLabel": "(n/a)",
107
+ "h2OptionsLabel": "staging / preprod / prod / main / dev / 사용자 입력",
108
+ "noMutationNote": "(mutating 명령 미실행 — H1=`skip` 또는 H3=`cancel`)",
109
+ "commandsTableHeader": {
110
+ "outputSummary": "stdout/stderr 요약"
111
+ }
112
+ },
113
+ "executionMeta": {
114
+ "runExecutorWorktreePath": "본 run 의 `EXECUTOR_WORKTREE_PATH`",
115
+ "runBaseRef": "본 run 의 base ref"
116
+ },
117
+ "evidenceMeta": {
118
+ "commitListSummary": "인용된 commit list / diff summary 요약",
119
+ "targetWorktreePath": "검증 대상 worktree path",
120
+ "capturedHeadBaseSha": "run 시작 시 capture 한 head/base SHA",
121
+ "gitStatusAtRunStart": "`git status --short` (run 시작 시점)"
122
+ },
123
+ "clarification": {
124
+ "fillAndRerun": "답을 채우신 뒤 같은 phase 를 다시 실행:",
125
+ "separateTerminalLabel": "별도 터미널",
126
+ "columnGuide": "컬럼 가이드 (전체 정의는 `prompts/profiles/_common-contract.md §Clarification request policy` SSOT 참조):"
127
+ },
128
+ "followUpTasks": {
129
+ "headingAside": "후속 작업"
130
+ },
131
+ "finalVerification": {
132
+ "validationEvidenceAside": "요구사항 커버리지",
133
+ "columnRequirement": "Requirement (plan/brief 인용)"
134
+ }
135
+ }
@@ -105,3 +105,21 @@ The final report of an `implementation-planning` run MUST contain every section
105
105
 
106
106
  - This input can be used as a planning draft before creating `okstra-task-brief.md`.
107
107
  - Reuse the same `Task Group` and `Task ID` if this plan belongs to the same long-lived task.
108
+
109
+ ## Stage Output Shape (reference)
110
+
111
+ This run's final report MUST emit `## 4.5 Stage Map` and `## 4.5.<i> Stage <i>` sections per the implementation-planning profile §"Required deliverable shape". Two illustrative shapes:
112
+
113
+ ### Shape A — single stage (small work)
114
+ | stage | title | depends-on | step-count | exit-contract-summary |
115
+ |-------|-------|-------|-------|-------|
116
+ | 1 | tiny rename | (none) | 2 | src/foo.ts:renamedFoo |
117
+
118
+ ### Shape B — three stages, two parallel
119
+ | stage | title | depends-on | step-count | exit-contract-summary |
120
+ |-------|-------|-------|-------|-------|
121
+ | 1 | foo API skeleton | (none) | 4 | src/foo/api.ts:exportedFoo |
122
+ | 2 | baz settings split | (none) | 2 | src/baz/settings.ts, env BAZ_MODE |
123
+ | 3 | bar integration | 1, 2 | 3 | src/bar/use-foo.ts, GET /bar |
124
+
125
+ Stages 1 and 2 in Shape B are `depends-on (none)` → can be run by two parallel `implementation` runs.
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: OKSTRA Improvement Discovery Input - {{TASK_KEY}}
3
+ id: {{FM_ID}}
4
+ tags: {{FM_TAGS}}
5
+ status: ready-for-agent
6
+ aliases: {{FM_ALIASES}}
7
+ date: {{TASK_DATE}}
8
+ task-id: "{{TASK_ID}}"
9
+ task-group: "{{TASK_GROUP}}"
10
+ project-id: "{{PROJECT_ID}}"
11
+ taskType: "{{FM_TASK_TYPE}}"
12
+ ---
13
+
14
+ # OKSTRA Improvement Discovery Input
15
+
16
+ ## Identity
17
+
18
+ - Project ID:
19
+ - Task Group:
20
+ - Task ID:
21
+ - Related Tasks:
22
+ - Issue / Ticket:
23
+ - 값이 비면 워커는 `Task ID` 로 폴백한다.
24
+ - Task Type: `improvement-discovery`
25
+ - Requested Outcome:
26
+
27
+ ## Scope (from brief frontmatter)
28
+
29
+ - scan-scope:
30
+ - out-of-scope:
31
+ - priority-lenses:
32
+ - candidate-cap (1..12, default 8):
33
+
34
+ ## Context
35
+
36
+ - Why this scope is being scanned now:
37
+ - Recent change context (last N commits to scan-scope):
38
+ - Stakeholders or owners of the scope:
39
+
40
+ ## Desired Outcome
41
+
42
+ - What kinds of improvements do you want surfaced?
43
+ - Anti-goals (improvements you do NOT want this run to propose):
44
+
45
+ ## Constraints
46
+
47
+ - Untouchable areas:
48
+ - Compatibility / deadline constraints:
49
+ - Performance / regression budget (if applicable):
50
+
51
+ ## Phase 1.5 — Lead Reflect-Back Grilling
52
+
53
+ This section is filled in by the lead during Phase 1.5 before worker dispatch.
54
+ Workers MUST read the resolved values from `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md`
55
+ rather than the unresolved brief.
56
+
57
+ - Reflect-back summary:
58
+ - Open questions (Q1..QN):
59
+ - Resolved scope:
60
+ - Resolved lenses:
61
+
62
+ ## Improvement Candidates (workers populate this)
63
+
64
+ | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
65
+ |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|
66
+
67
+ ## Questions for Analysers
68
+
69
+ 1. Within the resolved scope and priority lenses, what are the highest-impact improvement candidates?
70
+ 2. Which candidates have full cross-worker consensus, and which are worker-unique?
71
+ 3. For each candidate, what is the safest next phase (requirements-discovery / implementation-planning / error-analysis)?
72
+ 4. Which candidates would you intentionally exclude despite being technically valid, and why?
73
+ 5. Are there any signals that the scope itself is mis-defined (and should be re-narrowed before discovery proceeds)?
74
+
75
+ ## Conversion Note
76
+
77
+ - Each candidate the user picks becomes a new okstra task. Suggested task-key: `<task-group>/imp-<Cand-ID>`.
78
+ - The candidate row's Recommended next-phase determines which `--task-type` to launch with.
@@ -65,7 +65,7 @@ taskType: "{{FM_TASK_TYPE}}"
65
65
  - What trade-offs, dependencies, or migrations matter?
66
66
  - What validation and rollback approach is expected?
67
67
  - If `Task Type` is `implementation`:
68
- - Which approved `implementation-planning` final report authorises this run, and where is the explicit user-approval marker quoted?
68
+ - Which approved `implementation-planning` final report authorises this run, and is its frontmatter `approved: true` cited verbatim?
69
69
  - What is the authoritative file list and step order copied from that plan?
70
70
  - Which validation, TDD, and rollback commands must be executed and recorded with actual output?
71
71
  - If `Task Type` is `final-verification`:
@@ -141,7 +141,7 @@ taskType: "{{FM_TASK_TYPE}}"
141
141
  - Allowed and forbidden actions for each task type are listed in `Lifecycle Phase Boundaries` of the okstra skill (`agents/SKILL.md`). The lead and every worker stay inside that boundary.
142
142
  - "다음 단계 진행해" or any equivalent user phrase is interpreted as "complete the remaining outputs of the current phase," never as "start the next lifecycle phase." The next phase begins only via a fresh okstra invocation with the new `--task-type`.
143
143
  - For `implementation-planning` specifically: produce a plan document with the sections listed in `okstra-implementation-planning-input.template.md` `## Required Plan Deliverable`. Do not edit project source code, run builds/migrations/deployments, or write artifacts outside the run's own directories.
144
- - For `implementation` specifically: edits are bounded by the approved plan's file list (the `--approved-plan` reference). The run MUST refuse to start if the approved plan path is missing or has no explicit user-approval marker. `git push`, publish, deploy, real migrations, and any third-party write API call remain forbidden; only local `git add`/`git commit` are allowed. Verifier roles stay read-only — they record fix recommendations rather than applying edits — and acceptance verdicts belong to `final-verification`, not this phase.
144
+ - For `implementation` specifically: edits are bounded by the approved plan's file list (the `--approved-plan` reference). The run MUST refuse to start if the approved plan path is missing or its frontmatter `approved` field is not `true`. `git push`, publish, deploy, real migrations, and any third-party write API call remain forbidden; only local `git add`/`git commit` are allowed. Verifier roles stay read-only — they record fix recommendations rather than applying edits — and acceptance verdicts belong to `final-verification`, not this phase.
145
145
 
146
146
  ## Available MCP Servers
147
147
 
@@ -374,6 +374,36 @@ report_path.write_text("\n".join(report_lines) + "\n")
374
374
  import os
375
375
  WORKSPACE_ROOT = os.environ.get("OKSTRA_WORKSPACE_ROOT_FOR_FIXTURE", "")
376
376
  if WORKSPACE_ROOT:
377
+ # Write final-report .data.json SSOT next to the markdown. The validator's
378
+ # validate_final_report_data() reads this via _data_path_for(report_path)
379
+ # and the renderer treats it as the canonical source — the markdown alone
380
+ # is no longer a valid run artifact (dual-format final-report rollout).
381
+ # Sample bundled in tests/fixtures is patched with this run's task identity.
382
+ task_type = str(task_manifest.get("taskType", ""))
383
+ sample_path = (
384
+ Path(WORKSPACE_ROOT)
385
+ / "tests" / "fixtures" / "final-report-data"
386
+ / f"{task_type}-001.data.json"
387
+ )
388
+ if sample_path.is_file():
389
+ sample = json.loads(sample_path.read_text(encoding="utf-8"))
390
+ sample["frontmatter"]["taskGroup"] = str(task_manifest.get("taskGroup", ""))
391
+ sample["frontmatter"]["taskId"] = str(task_manifest.get("taskId", ""))
392
+ sample["frontmatter"]["taskType"] = task_type
393
+ sample["frontmatter"]["projectId"] = str(task_manifest.get("projectId", ""))
394
+ sample["header"]["taskKey"] = str(task_manifest.get("taskKey", ""))
395
+ sample["header"]["taskType"] = task_type
396
+ name = report_path.name
397
+ data_path = (
398
+ report_path.with_name(name[:-3] + ".data.json")
399
+ if name.endswith(".md")
400
+ else report_path.with_suffix(".data.json")
401
+ )
402
+ data_path.write_text(
403
+ json.dumps(sample, indent=2, ensure_ascii=False) + "\n",
404
+ encoding="utf-8",
405
+ )
406
+
377
407
  import sys as _sys
378
408
  _sys.path.insert(0, str(Path(WORKSPACE_ROOT) / "scripts"))
379
409
  try:
@@ -41,7 +41,7 @@ run_validator_expectation() {
41
41
 
42
42
  expected_task_manifest_relative_path="$(task_manifest_relative_path "$task_group" "$task_id")"
43
43
 
44
- python3 - "$PROJECT_ROOT" "$expected_task_manifest_relative_path" "$RUN_VALIDATOR_SCRIPT" "$expected_status" "$expected_failure_substring" <<'PY'
44
+ python3 - "$PROJECT_ROOT" "$expected_task_manifest_relative_path" "$RUN_VALIDATOR_PATH" "$expected_status" "$expected_failure_substring" <<'PY'
45
45
  from pathlib import Path
46
46
  import json
47
47
  import subprocess
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """S1–S8 checks for the Stage Map structure of an approved
3
+ implementation-planning final-report.md. Run from prepare_task_bundle
4
+ of `implementation` task or standalone."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import re
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import List, Tuple
14
+
15
+ STAGE_MAP_HEADING = re.compile(r"^##\s+4\.5\s+Stage\s+Map\b", re.M)
16
+ STAGE_SECTION = re.compile(
17
+ r"^##\s+4\.5\.(\d+)\s+Stage\s+\1\s*:\s*(.+)$", re.M
18
+ )
19
+ REQUIRED_SUBSECTIONS = (
20
+ "Carry-In",
21
+ "Stepwise Execution Order",
22
+ "Stage Exit Contract",
23
+ "Stage Validation",
24
+ )
25
+
26
+
27
+ @dataclass
28
+ class StageMeta:
29
+ stage_number: int
30
+ title: str
31
+ depends_on: List[int]
32
+ step_count: int
33
+ exit_contract_summary: str
34
+
35
+
36
+ @dataclass
37
+ class ValidationError:
38
+ code: str # S1..S8
39
+ stage: int # 0 = global
40
+ message: str
41
+
42
+
43
+ def _check_stage_map_present(text: str) -> List[ValidationError]:
44
+ if not STAGE_MAP_HEADING.search(text):
45
+ return [ValidationError("S1", 0,
46
+ "section '## 4.5 Stage Map' is missing")]
47
+ return []
48
+
49
+
50
+ def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]:
51
+ m = STAGE_MAP_HEADING.search(text)
52
+ if not m:
53
+ return [], [] # S1 already reported
54
+ body = text[m.end():]
55
+ rows = []
56
+ for line in body.splitlines():
57
+ if line.startswith("##"):
58
+ break
59
+ if not line.strip().startswith("|"):
60
+ continue
61
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
62
+ if len(cells) != 5:
63
+ continue
64
+ # skip header and separator rows (all-dash of any length is covered by set check)
65
+ if cells[0] == "stage" or set(cells[0]) <= set("-"):
66
+ continue
67
+ try:
68
+ n = int(cells[0])
69
+ except ValueError:
70
+ continue
71
+ depends_raw = cells[2].strip()
72
+ depends = [] if depends_raw in ("(none)", "") else [
73
+ int(x.strip()) for x in depends_raw.split(",") if x.strip()
74
+ ]
75
+ try:
76
+ step_count = int(cells[3])
77
+ except ValueError:
78
+ step_count = -1
79
+ rows.append(StageMeta(n, cells[1], depends, step_count, cells[4]))
80
+ errors: List[ValidationError] = []
81
+ for i, r in enumerate(rows, start=1):
82
+ if r.stage_number != i:
83
+ errors.append(ValidationError("S2", r.stage_number,
84
+ f"stage numbers must be 1..N monotonic, got {r.stage_number} at row {i}"))
85
+ return rows, errors
86
+
87
+
88
+ def _count_effective_steps(section: str) -> int:
89
+ m = re.search(r"^###\s+Stepwise Execution Order\b", section, re.M)
90
+ if not m:
91
+ return 0
92
+ body = section[m.end():]
93
+ nxt = re.search(r"^###\s+\w", body, re.M)
94
+ if nxt:
95
+ body = body[: nxt.start()]
96
+ count = 0
97
+ for line in body.splitlines():
98
+ s = line.strip()
99
+ if not s or s.startswith("<!--"):
100
+ continue
101
+ if not s.startswith("|"):
102
+ continue
103
+ # Reuse the same header/divider detection as _parse_stage_map:
104
+ # split on `|`, inspect first non-empty cell.
105
+ first_cell = s.strip("|").split("|")[0].strip()
106
+ if first_cell.lower() == "step":
107
+ continue
108
+ if set(first_cell) <= set("-: "):
109
+ continue
110
+ count += 1
111
+ return count
112
+
113
+
114
+ def _check_each_stage_section(text: str, stages: List[StageMeta]) -> List[ValidationError]:
115
+ errs: List[ValidationError] = []
116
+ for s in stages:
117
+ pattern = rf"^##\s+4\.5\.{s.stage_number}\s+Stage\s+{s.stage_number}\s*:"
118
+ start_m = re.search(pattern, text, re.M)
119
+ if not start_m:
120
+ errs.append(ValidationError("S3", s.stage_number,
121
+ f"stage section '## 4.5.{s.stage_number} Stage {s.stage_number}:' missing"))
122
+ continue
123
+ # Slice the stage's section body
124
+ start = start_m.end()
125
+ nxt = re.search(
126
+ rf"^##\s+4\.5\.{s.stage_number + 1}\s+Stage\s+",
127
+ text[start:], re.M,
128
+ )
129
+ section = text[start: start + nxt.start()] if nxt else text[start:]
130
+
131
+ for sub in REQUIRED_SUBSECTIONS:
132
+ if not re.search(rf"^###\s+{re.escape(sub)}\b", section, re.M):
133
+ errs.append(ValidationError("S4", s.stage_number,
134
+ f"required subsection '### {sub}' missing"))
135
+
136
+ # S5: effective step count
137
+ steps = _count_effective_steps(section)
138
+ if steps > 6:
139
+ errs.append(ValidationError("S5", s.stage_number,
140
+ f"effective step count {steps} exceeds 6"))
141
+
142
+ # S7: step-count cell vs. real count
143
+ if s.step_count >= 0 and s.step_count != steps:
144
+ errs.append(ValidationError("S7", s.stage_number,
145
+ f"Stage Map step-count={s.step_count} but real count={steps}"))
146
+ return errs
147
+
148
+
149
+ def _check_depends_on(stages: List[StageMeta]) -> List[ValidationError]:
150
+ errs: List[ValidationError] = []
151
+ valid = {s.stage_number for s in stages}
152
+ for s in stages:
153
+ for d in s.depends_on:
154
+ if d == s.stage_number:
155
+ errs.append(ValidationError("S8", s.stage_number, "self depends-on"))
156
+ elif d not in valid:
157
+ errs.append(ValidationError("S6", s.stage_number,
158
+ f"depends-on {d} does not exist"))
159
+
160
+ # DAG cycle detection via Kahn's algorithm
161
+ # Build a graph only from valid edges (S6 errors already reported above)
162
+ indeg = {s.stage_number: 0 for s in stages}
163
+ graph: dict[int, list[int]] = {s.stage_number: [] for s in stages}
164
+ for s in stages:
165
+ for d in s.depends_on:
166
+ if d in graph and d != s.stage_number:
167
+ graph[d].append(s.stage_number)
168
+ indeg[s.stage_number] += 1
169
+
170
+ queue = [n for n, k in indeg.items() if k == 0]
171
+ visited = 0
172
+ while queue:
173
+ n = queue.pop()
174
+ visited += 1
175
+ for m in graph[n]:
176
+ indeg[m] -= 1
177
+ if indeg[m] == 0:
178
+ queue.append(m)
179
+ if visited != len(stages):
180
+ errs.append(ValidationError("S8", 0, "depends-on graph has a cycle"))
181
+ return errs
182
+
183
+
184
+ def main(argv: List[str]) -> int:
185
+ p = argparse.ArgumentParser()
186
+ p.add_argument("--plan", required=True)
187
+ args = p.parse_args(argv)
188
+ text = Path(args.plan).read_text(encoding="utf-8")
189
+
190
+ errors: List[ValidationError] = []
191
+ errors.extend(_check_stage_map_present(text))
192
+ if errors:
193
+ for e in errors:
194
+ print(f"{e.code} stage={e.stage}: {e.message}", file=sys.stderr)
195
+ return 1
196
+
197
+ stages, s2_errs = _parse_stage_map(text)
198
+ errors.extend(s2_errs)
199
+ if stages:
200
+ errors.extend(_check_each_stage_section(text, stages))
201
+ errors.extend(_check_depends_on(stages))
202
+
203
+ if errors:
204
+ for e in errors:
205
+ print(f"{e.code} stage={e.stage}: {e.message}", file=sys.stderr)
206
+ return 1
207
+ return 0
208
+
209
+
210
+ if __name__ == "__main__":
211
+ sys.exit(main(sys.argv[1:]))