okstra 0.38.1 → 0.40.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 (80) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/kr/architecture.md +18 -2
  4. package/docs/kr/cli.md +1 -1
  5. package/docs/project-structure-overview.md +2 -3
  6. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +326 -0
  7. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +337 -0
  8. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +410 -0
  9. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +728 -0
  10. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +113 -0
  11. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +173 -0
  12. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +154 -0
  13. package/docs/task-process/requirements-discovery.md +1 -1
  14. package/package.json +3 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/{python → bin}/lib/okstra/usage.sh +3 -2
  17. package/runtime/bin/okstra-codex-exec.sh +3 -3
  18. package/runtime/bin/okstra-trace-cleanup.sh +64 -26
  19. package/runtime/prompts/profiles/_common-contract.md +9 -5
  20. package/runtime/prompts/profiles/final-verification.md +18 -16
  21. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  22. package/runtime/prompts/profiles/requirements-discovery.md +18 -1
  23. package/runtime/prompts/wizard/prompts.ko.json +11 -0
  24. package/runtime/python/okstra_ctl/consumers.py +1 -1
  25. package/runtime/python/okstra_ctl/fanout.py +35 -0
  26. package/runtime/python/okstra_ctl/migrate.py +21 -42
  27. package/runtime/python/okstra_ctl/reconcile.py +2 -2
  28. package/runtime/python/okstra_ctl/render_final_report.py +0 -1
  29. package/runtime/python/okstra_ctl/run.py +0 -29
  30. package/runtime/python/okstra_ctl/run_context.py +9 -12
  31. package/runtime/python/okstra_ctl/seeding.py +0 -192
  32. package/runtime/python/okstra_ctl/wizard.py +70 -5
  33. package/runtime/python/okstra_ctl/work_categories.py +21 -0
  34. package/runtime/python/okstra_ctl/worktree.py +74 -77
  35. package/runtime/python/okstra_project/__init__.py +0 -6
  36. package/runtime/python/okstra_project/dirs.py +0 -8
  37. package/runtime/schemas/final-report-v1.0.schema.json +34 -27
  38. package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
  39. package/runtime/skills/okstra-convergence/SKILL.md +1 -1
  40. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  41. package/runtime/skills/okstra-run/SKILL.md +2 -0
  42. package/runtime/templates/prd/brief.template.md +1 -1
  43. package/runtime/templates/reports/fan-out-unit.template.md +25 -0
  44. package/runtime/templates/reports/final-report.template.md +24 -13
  45. package/runtime/templates/reports/final-verification-input.template.md +16 -5
  46. package/runtime/templates/reports/i18n/en.json +6 -3
  47. package/runtime/templates/reports/i18n/ko.json +6 -3
  48. package/runtime/templates/worker-prompt-preamble.md +7 -0
  49. package/runtime/validators/lib/fixtures.sh +2 -2
  50. package/runtime/validators/lib/validate-assets.sh +9 -0
  51. package/runtime/validators/validate-implementation-plan-stages.py +19 -11
  52. package/runtime/validators/validate-run.py +114 -0
  53. package/runtime/validators/validate-schedule.py +4 -4
  54. package/runtime/validators/validate_fanout.py +99 -0
  55. package/src/_proc.mjs +31 -0
  56. package/src/check-project.mjs +1 -25
  57. package/src/config.mjs +7 -31
  58. package/src/doctor.mjs +10 -29
  59. package/src/install.mjs +8 -36
  60. package/src/migrate.mjs +1 -18
  61. package/src/okstra-dirs.mjs +0 -11
  62. package/src/setup.mjs +1 -154
  63. package/src/uninstall.mjs +6 -13
  64. package/runtime/templates/okstra.CLAUDE.md +0 -104
  65. /package/runtime/{python → bin}/lib/okstra/cli.sh +0 -0
  66. /package/runtime/{python → bin}/lib/okstra/globals.sh +0 -0
  67. /package/runtime/{python → bin}/lib/okstra/interactive.sh +0 -0
  68. /package/runtime/{python → bin}/lib/okstra/project-resolver.sh +0 -0
  69. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-batch.sh +0 -0
  70. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-list.sh +0 -0
  71. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-open.sh +0 -0
  72. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-projects.sh +0 -0
  73. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reconcile.sh +0 -0
  74. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reindex.sh +0 -0
  75. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-rerun.sh +0 -0
  76. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-show.sh +0 -0
  77. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-tail.sh +0 -0
  78. /package/runtime/{python → bin}/lib/okstra-ctl/main.sh +0 -0
  79. /package/runtime/{python → bin}/lib/okstra-ctl/prepare.sh +0 -0
  80. /package/runtime/{python → bin}/lib/okstra-ctl/usage.sh +0 -0
@@ -293,7 +293,11 @@
293
293
  "items": { "type": "string" },
294
294
  "minItems": 1
295
295
  },
296
- "nextStep": { "type": "string", "minLength": 1 }
296
+ "nextStep": { "type": "string", "minLength": 1 },
297
+ "conditionalAcceptanceConditions": {
298
+ "type": "array",
299
+ "items": { "$ref": "#/$defs/ConditionalAcceptanceConditionRow" }
300
+ }
297
301
  }
298
302
  },
299
303
 
@@ -544,14 +548,17 @@
544
548
  "properties": {
545
549
  "sourceImplementationReport": {
546
550
  "type": "object",
547
- "required": ["path", "commitListQuote", "worktreePath", "baseHeadSha", "gitStatusShort"],
551
+ "required": ["path", "commitListQuote", "diffSummaryQuote", "worktreePath", "implementationBaseRef", "capturedHeadSha", "gitStatusShort", "gitDiffStat"],
548
552
  "additionalProperties": false,
549
553
  "properties": {
550
554
  "path": { "type": "string" },
551
555
  "commitListQuote": { "type": "string" },
556
+ "diffSummaryQuote": { "type": "string" },
552
557
  "worktreePath": { "type": "string" },
553
- "baseHeadSha": { "type": "string" },
554
- "gitStatusShort": { "type": "string" }
558
+ "implementationBaseRef": { "type": "string" },
559
+ "capturedHeadSha": { "type": "string" },
560
+ "gitStatusShort": { "type": "string" },
561
+ "gitDiffStat": { "type": "string" }
555
562
  }
556
563
  },
557
564
  "acceptanceBlockers": {
@@ -1092,7 +1099,7 @@
1092
1099
 
1093
1100
  "PlanBodyVerification": {
1094
1101
  "type": "object",
1095
- "required": ["roundCount", "gateResult", "verdictSummary", "verdictDetails", "dissentLog"],
1102
+ "required": ["roundCount", "gateResult", "verdictDetails", "dissentLog"],
1096
1103
  "additionalProperties": false,
1097
1104
  "properties": {
1098
1105
  "roundCount": { "type": "integer", "minimum": 0 },
@@ -1104,26 +1111,6 @@
1104
1111
  "aborted-non-result"
1105
1112
  ]
1106
1113
  },
1107
- "verdictSummary": {
1108
- "type": "array",
1109
- "items": {
1110
- "type": "object",
1111
- "required": ["planItem", "ticketId", "section", "classification"],
1112
- "additionalProperties": false,
1113
- "properties": {
1114
- "planItem": {
1115
- "type": "string",
1116
- "pattern": "^P-(Opt|Step|Dep|Val|Rb)-\\d+$"
1117
- },
1118
- "ticketId": { "$ref": "#/$defs/TicketId" },
1119
- "section": { "type": "string", "minLength": 1 },
1120
- "classification": {
1121
- "type": "string",
1122
- "pattern": "^(full-consensus|partial-consensus|worker-unique|majority-disagree → C-\\d+)$"
1123
- }
1124
- }
1125
- }
1126
- },
1127
1114
  "verdictDetails": {
1128
1115
  "type": "array",
1129
1116
  "items": {
@@ -1305,14 +1292,34 @@
1305
1292
 
1306
1293
  "ReadonlyCommandRow": {
1307
1294
  "type": "object",
1308
- "required": ["number", "tier", "command", "exitCode", "outputTail"],
1295
+ "required": ["number", "tier", "command", "status", "exitCode", "outputTail"],
1309
1296
  "additionalProperties": false,
1310
1297
  "properties": {
1311
1298
  "number": { "type": "integer", "minimum": 1 },
1312
1299
  "tier": { "enum": [1, 2] },
1313
1300
  "command": { "type": "string", "minLength": 1 },
1314
- "exitCode": { "type": "integer" },
1301
+ "status": { "enum": ["executed", "rejected", "not-configured"] },
1302
+ "exitCode": { "type": ["integer", "null"] },
1303
+ "rejectionReason": { "type": ["string", "null"] },
1315
1304
  "outputTail": { "type": "string" }
1305
+ },
1306
+ "allOf": [
1307
+ { "if": { "properties": { "status": { "const": "executed" } } },
1308
+ "then": { "properties": { "exitCode": { "type": "integer" } } } },
1309
+ { "if": { "properties": { "status": { "const": "rejected" } } },
1310
+ "then": { "required": ["rejectionReason"] } }
1311
+ ]
1312
+ },
1313
+
1314
+ "ConditionalAcceptanceConditionRow": {
1315
+ "type": "object",
1316
+ "required": ["id", "condition", "evidenceRequired", "blocksReleaseHandoff"],
1317
+ "additionalProperties": false,
1318
+ "properties": {
1319
+ "id": { "type": "string", "pattern": "^CA-\\d{3,}$" },
1320
+ "condition": { "type": "string", "minLength": 1 },
1321
+ "evidenceRequired": { "type": "string", "minLength": 1 },
1322
+ "blocksReleaseHandoff": { "type": "boolean" }
1316
1323
  }
1317
1324
  },
1318
1325
 
@@ -44,7 +44,7 @@ user-invocable: false
44
44
  | `taskGroup` | Task group |
45
45
  | `taskId` | Task ID |
46
46
  | `taskType` | Analysis type (requirements-discovery, error-analysis, implementation-planning, implementation, final-verification, release-handoff) |
47
- | `workCategory` | bugfix / feature / improvement / refactor / ops-change / unknown |
47
+ | `workCategory` | bugfix / feature / improvement / refactor / ops / unknown |
48
48
  | `recommendedWorkers` | List of selected workers |
49
49
  | `currentStatus` | Current task status |
50
50
  | `workflow.phaseSequence` | Ordered lifecycle phases for the task |
@@ -507,7 +507,7 @@ Plan-body verification only supports **lightweight mode** (defined in §"Verific
507
507
  - all dispatches non-result → `aborted-non-result`
508
508
  - any `partial-consensus` / `dissent-isolated` present, no `majority-disagree` → `passed-with-dissent`
509
509
  - all items `full-consensus` → `passed`
510
- 6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 4.5.9 Plan Body Verification` in the final report (template at `templates/reports/final-report.template.md`). The §4.5.9 body in the template is split into two tables: a narrow `#### Verdict summary` (`Plan item / Ticket ID / Section / Classification` only — one row per plan item) and a tall `#### Verdict details` (`Plan item / Worker / Verdict / Breakage kind / Note` — one row per plan-item × worker pair). The older wide `| Plan item | <worker1> | <worker2> | … | Classification |` matrix is removed — it scaled horizontally with the worker count and lost readability past 3 workers. Lead MUST emit both tables (the validator's `Plan Body Verification` + `Gate result:` substring checks still pass either layout, but reviewers depend on the split form).
510
+ 6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 4.5.9 Plan Body Verification` in the final report (template at `templates/reports/final-report.template.md`). The §4.5.9 body uses a single `#### Verdict details` table (`Plan item / Worker / Verdict / Breakage kind / Note` — one row per plan-item × worker pair). The older wide `| Plan item | <worker1> | <worker2> | … | Classification |` matrix and the former narrow `#### Verdict summary` card are both removed — the matrix scaled horizontally with the worker count, and the summary only restated per-item classifications already derivable from the details table. The validator's `Plan Body Verification` + `Gate result:` substring checks gate this section.
511
511
  7. For every `majority-disagree` item, lead adds a row to `## 5. Clarification Items` with:
512
512
  - new `C-<N>` ID (numbering continues from any existing rows)
513
513
  - `Statement` summarising the disagreement and the worker breakage `<kind>`
@@ -51,7 +51,7 @@ Read `.okstra/discovery/task-catalog.json`. The catalog is the authoritative sou
51
51
  |------|------|
52
52
  | `taskKey` | `<project-id>:<task-group>:<task-id>` |
53
53
  | `taskType` | latest task type |
54
- | `workCategory` | bugfix / feature / improvement / refactor / ops-change / unknown. Display `unknown` as-is, but flag with `(unset)` annotation so the reader knows the requirements-discovery classification was skipped or no `--work-category` flag was passed. |
54
+ | `workCategory` | bugfix / feature / improvement / refactor / ops / unknown. Display `unknown` as-is, but flag with `(unset)` annotation so the reader knows the requirements-discovery classification was skipped or no `--work-category` flag was passed. |
55
55
  | `currentStatus` | task-level status |
56
56
  | `currentPhase` | lifecycle current phase |
57
57
  | `currentPhaseState` | lifecycle phase state |
@@ -45,6 +45,8 @@ The wizard tells you *which UI to use* via `kind` (and the optional `multi` flag
45
45
  - `kind: "text"` → write `label` as a plain text message and consume the user's NEXT message as the answer.
46
46
  - `kind: "done"` → input collection finished; move to Step 5.
47
47
 
48
+ The `branch_confirm` step (shown just before `confirm`) is a normal `pick` step and is rendered the same way — no special handling needed.
49
+
48
50
  Never invent additional questions. Never reorder. Never use `AskUserQuestion` for `text` prompts — the wizard explicitly chose `text` to avoid the picker-Other re-render lag.
49
51
 
50
52
  ## Step 1: Verify okstra runtime + project setup
@@ -138,7 +138,7 @@ Codex/Gemini는 이 코드베이스를 모릅니다. 핵심 용어 5-15개를
138
138
  <증거 또는 "weak signal — none observed">
139
139
  - **Why might this be a feature/improvement?**
140
140
  <증거>
141
- - **Why might this be a refactor/ops-change?**
141
+ - **Why might this be a refactor/ops?**
142
142
  <증거>
143
143
  - **Classification blockers** — 무엇이 분류 확신을 막고 있는가?
144
144
  <evidence gap을 구체적으로>
@@ -0,0 +1,25 @@
1
+ <!-- templates/reports/fan-out-unit.template.md -->
2
+ ---
3
+ unit-id: {{UNIT_ID}}
4
+ domain: {{DOMAIN}}
5
+ <!-- depends-on 예: [unit-001] (의존 없으면 []) -->
6
+ depends-on: {{DEPENDS_ON}}
7
+ recommended-next-phase: {{NEXT_PHASE}}
8
+ ---
9
+
10
+ # Fan-out Unit: {{UNIT_ID}} ({{DOMAIN}})
11
+
12
+ > requirements-discovery fan-out 산출 packet. `okstra-run --task-brief <이 파일 경로>`
13
+ > 로 새 task-key 를 시작한다. 이 파일은 그 run 의 입력 packet 이다.
14
+
15
+ ## Scope
16
+
17
+ <!-- 이 단위가 다루는 작업 항목 1개를 자족적으로 서술. 다른 단위와 섞지 말 것. -->
18
+
19
+ ## Evidence
20
+
21
+ <!-- path:line 근거. requirements-discovery 가 file inspection 으로 확인한 위치. -->
22
+
23
+ ## Depends-on rationale
24
+
25
+ <!-- depends-on 에 적은 각 unit 에 왜 의존하는지 1줄씩. 없으면 _(none)_ -->
@@ -286,14 +286,6 @@ approved: {{ frontmatter.approved | yaml_scalar }}
286
286
  - **Round count**: `{{ implementationPlanning.planBodyVerification.roundCount }}`
287
287
  - **Gate result**: `{{ implementationPlanning.planBodyVerification.gateResult }}`
288
288
 
289
- #### Verdict summary
290
-
291
- | Plan item | Ticket ID | Section | Classification |
292
- |-----------|-----------|---------|----------------|
293
- {% for row in implementationPlanning.planBodyVerification.verdictSummary -%}
294
- | {{ row.planItem }} | `{{ row.ticketId }}` | {{ row.section }} | {{ row.classification }} |
295
- {% endfor %}
296
-
297
289
  #### Verdict details
298
290
 
299
291
  | Plan item | Worker | Verdict | Breakage kind | Note |
@@ -479,12 +471,19 @@ approved: {{ frontmatter.approved | yaml_scalar }}
479
471
  - Path (project-relative): `{{ finalVerification.sourceImplementationReport.path }}`
480
472
  - {{ t("evidenceMeta.commitListSummary") }}:
481
473
  > {{ finalVerification.sourceImplementationReport.commitListQuote }}
474
+ - {{ t("evidenceMeta.diffSummaryQuote") }}:
475
+ > {{ finalVerification.sourceImplementationReport.diffSummaryQuote }}
482
476
  - {{ t("evidenceMeta.targetWorktreePath") }}: `{{ finalVerification.sourceImplementationReport.worktreePath }}`
483
- - {{ t("evidenceMeta.capturedHeadBaseSha") }}: `{{ finalVerification.sourceImplementationReport.baseHeadSha }}`
477
+ - {{ t("evidenceMeta.implementationBaseRef") }}: `{{ finalVerification.sourceImplementationReport.implementationBaseRef }}`
478
+ - {{ t("evidenceMeta.capturedHeadSha") }}: `{{ finalVerification.sourceImplementationReport.capturedHeadSha }}`
484
479
  - {{ t("evidenceMeta.gitStatusAtRunStart") }}:
485
480
  ```
486
481
  {{ finalVerification.sourceImplementationReport.gitStatusShort }}
487
482
  ```
483
+ - {{ t("evidenceMeta.gitDiffStatAtRunStart") }}:
484
+ ```
485
+ {{ finalVerification.sourceImplementationReport.gitDiffStat }}
486
+ ```
488
487
 
489
488
  ### 4.8.2 Acceptance Blockers
490
489
 
@@ -520,13 +519,25 @@ approved: {{ frontmatter.approved | yaml_scalar }}
520
519
 
521
520
  ### 4.8.5 Read-only Command Log
522
521
 
523
- | # | Tier | Command (verbatim) | Exit code | Output tail |
524
- |---|------|---------------------|-----------|-------------|
522
+ | # | Tier | Command (verbatim) | Status | Exit code | Output tail |
523
+ |---|------|---------------------|--------|-----------|-------------|
525
524
  {% for row in finalVerification.readonlyCommandLog -%}
526
- | {{ row.number }} | {{ row.tier }} | `{{ row.command }}` | `{{ row.exitCode }}` | {{ row.outputTail }} |
525
+ | {{ row.number }} | {{ row.tier }} | `{{ row.command }}` | `{{ row.status }}` | {{ row.exitCode if row.exitCode is not none else '—' }} | {{ row.rejectionReason if row.status == 'rejected' else row.outputTail }} |
527
526
  {% endfor %}
528
527
 
529
- ### 4.8.6 Routing Recommendation
528
+ ### 4.8.6 Conditional Acceptance Conditions
529
+
530
+ {% if not finalVerdict.conditionalAcceptanceConditions -%}
531
+ - Not applicable (verdict is not `conditional-accept`).
532
+ {%- else %}
533
+ | ID | Condition | Evidence required | Blocks release-handoff |
534
+ |----|-----------|-------------------|------------------------|
535
+ {% for row in finalVerdict.conditionalAcceptanceConditions -%}
536
+ | {{ row.id }} | {{ row.condition }} | {{ row.evidenceRequired }} | {{ row.blocksReleaseHandoff }} |
537
+ {% endfor %}
538
+ {%- endif %}
539
+
540
+ ### 4.8.7 Routing Recommendation
530
541
 
531
542
  {{ finalVerification.routingRecommendation }}
532
543
 
@@ -39,6 +39,15 @@ taskType: "{{FM_TASK_TYPE}}"
39
39
 
40
40
  > If this section is empty, points to a missing report, or names a checkout that does not match the implementation report's commit list / diff summary, final-verification MUST end with status `blocked` and route back to `implementation` or `implementation-planning`. Do not verify an ambiguous target.
41
41
 
42
+ ## Requirement Coverage Source
43
+
44
+ - Approved implementation-planning report path:
45
+ - Requirement source used for coverage (plan section / brief Acceptance Criteria):
46
+ - Requirement IDs / acceptance IDs to verify:
47
+ - Requirements intentionally excluded from this verification:
48
+
49
+ > final-verification 은 위 source 의 각 requirement / acceptance id 마다 Validation Evidence 에 artifact 를 cite 해야 한다. source 가 비면 brief 의 `## Acceptance Criteria` 를 기본 source 로 사용한다.
50
+
42
51
  ## Verification Evidence
43
52
 
44
53
  - PR or change summary:
@@ -78,11 +87,13 @@ taskType: "{{FM_TASK_TYPE}}"
78
87
 
79
88
  ## Questions for Analysers
80
89
 
81
- 1. Are there any acceptance blockers?
82
- 2. What residual risks remain?
83
- 3. Is additional validation needed before release?
84
- 4. Which acceptance checks did you consider and **deliberately exclude** from this verification, and why must each be a separate verification task instead of being folded in here?
85
- 5. Did any check go beyond `Acceptance Criteria` (e.g., quality improvements, unrelated regressions)? If yes, separate them from the pass/fail decision and report as follow-up only.
90
+ 1. Does the verification target (head SHA / diff stat) match the implementation report's commit list and diff summary?
91
+ 2. For each requirement / acceptance criterion, what exact artifact (commit SHA, test output, log line, config value) proves coverage?
92
+ 3. Are there any acceptance blockers?
93
+ 4. What residual risks remain?
94
+ 5. Is additional validation needed before release?
95
+ 6. Which acceptance checks did you consider and **deliberately exclude** from this verification, and why must each be a separate verification task instead of being folded in here?
96
+ 7. Did any check go beyond `Acceptance Criteria` (e.g., quality improvements, unrelated regressions)? If yes, separate them from the pass/fail decision and report as follow-up only.
86
97
 
87
98
  ## Conversion Note
88
99
 
@@ -117,10 +117,13 @@
117
117
  "runBaseRef": "This run's base ref"
118
118
  },
119
119
  "evidenceMeta": {
120
- "commitListSummary": "Cited commit list / diff summary",
120
+ "commitListSummary": "Cited commit list",
121
+ "diffSummaryQuote": "Cited diff summary (from implementation report)",
121
122
  "targetWorktreePath": "Target worktree path for verification",
122
- "capturedHeadBaseSha": "head/base SHA captured at run start",
123
- "gitStatusAtRunStart": "`git status --short` (at run start)"
123
+ "implementationBaseRef": "Implementation base ref",
124
+ "capturedHeadSha": "Head SHA captured at run start",
125
+ "gitStatusAtRunStart": "`git status --short` (at run start)",
126
+ "gitDiffStatAtRunStart": "`git diff --stat <base>..HEAD` (at run start)"
124
127
  },
125
128
  "clarification": {
126
129
  "fillAndRerun": "Fill in your answers then re-run the same phase:",
@@ -117,10 +117,13 @@
117
117
  "runBaseRef": "본 run 의 base ref"
118
118
  },
119
119
  "evidenceMeta": {
120
- "commitListSummary": "인용된 commit list / diff summary 요약",
120
+ "commitListSummary": "인용된 commit list",
121
+ "diffSummaryQuote": "인용된 diff summary (implementation 보고서)",
121
122
  "targetWorktreePath": "검증 대상 worktree path",
122
- "capturedHeadBaseSha": "run 시작 시 capture 한 head/base SHA",
123
- "gitStatusAtRunStart": "`git status --short` (run 시작 시점)"
123
+ "implementationBaseRef": "Implementation base ref",
124
+ "capturedHeadSha": "run 시작 capture head SHA",
125
+ "gitStatusAtRunStart": "`git status --short` (run 시작 시점)",
126
+ "gitDiffStatAtRunStart": "`git diff --stat <base>..HEAD` (run 시작 시점)"
124
127
  },
125
128
  "clarification": {
126
129
  "fillAndRerun": "답을 채우신 뒤 같은 phase 를 다시 실행:",
@@ -86,6 +86,13 @@ For the **implementation phase** specifically, the dispatched prompt MUST also i
86
86
  - `**Worktree:** <absolute-path>` — the task worktree path.
87
87
  - `cwd for every mutating command: <absolute-path>` — same as Worktree path; used by codex / gemini wrappers as `--add-dir` / `--include-directories`.
88
88
 
89
+ For the **final-verification phase** specifically, the dispatched prompt MUST also include the verification target snapshot so every analyser verifies the SAME target the lead captured at the entry gate:
90
+
91
+ - `**Worktree:** <absolute-path>` — the checkout under verification (read-only).
92
+ - `**Verification base ref:** <base-ref>` — the implementation base for the diff.
93
+ - `**Verification head SHA:** <sha>` — head SHA captured at run start.
94
+ - `**Verification diff stat:** <git diff --stat output>` — the captured diff-stat that bounds the verification scope.
95
+
89
96
  When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.
90
97
 
91
98
  ---
@@ -359,8 +359,8 @@ report_lines.extend(
359
359
  "## 4.8 Final Verification Deliverables",
360
360
  "",
361
361
  "Source Implementation Report / Acceptance Blockers / Residual Risk / "
362
- "Validation Evidence / Read-only Command Log / Routing Recommendation: "
363
- "fixture stub.",
362
+ "Validation Evidence / Read-only Command Log / Conditional Acceptance "
363
+ "Conditions / Routing Recommendation: fixture stub.",
364
364
  ]
365
365
  )
366
366
  report_path.parent.mkdir(parents=True, exist_ok=True)
@@ -40,12 +40,21 @@ def check(source_path: Path, target_path: Path) -> None:
40
40
 
41
41
 
42
42
  # 1. Worker agent files: agents/workers/*-worker.md -> runtime/agents/workers/*-worker.md
43
+ #
44
+ # `_cli-wrapper-template.md` is a build-time render INPUT, not a shipped
45
+ # artifact — tools/build.mjs renders it (with each *.params.json) into
46
+ # codex-worker.md / gemini-worker.md and excludes the template itself from the
47
+ # runtime payload. Skip it here so parity is checked only on shipped files.
48
+ # Keep this set in sync with TEMPLATE_INPUT_BASENAMES in tools/build.mjs.
49
+ template_input_basenames = {"_cli-wrapper-template.md"}
43
50
  workers_source = agents_source_root / "workers"
44
51
  workers_target = runtime_root / "agents" / "workers"
45
52
  if not workers_source.is_dir():
46
53
  errors.append(f"missing agents/workers source directory: {workers_source}")
47
54
  else:
48
55
  for source_path in sorted(workers_source.glob("*.md")):
56
+ if source_path.name in template_input_basenames:
57
+ continue
49
58
  check(source_path, workers_target / source_path.name)
50
59
 
51
60
  # 2. Lead agent SKILL.md: agents/SKILL.md -> runtime/agents/SKILL.md
@@ -181,25 +181,33 @@ def _check_depends_on(stages: List[StageMeta]) -> List[ValidationError]:
181
181
  return errs
182
182
 
183
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")
184
+ def collect_validation_errors(text: str) -> List[ValidationError]:
185
+ """All S1–S8 checks against the report text; empty list means valid.
189
186
 
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
187
+ S1 (missing `## 4.5 Stage Map` heading) makes the rest unparseable, so it
188
+ short-circuits. Shared by `main()` (CLI / implementation entry) and the
189
+ implementation-planning boundary check in validate-run.py — one validator,
190
+ one reference point, enforced at both produce-time and consume-time."""
191
+ present = _check_stage_map_present(text)
192
+ if present:
193
+ return present
196
194
 
195
+ errors: List[ValidationError] = []
197
196
  stages, s2_errs = _parse_stage_map(text)
198
197
  errors.extend(s2_errs)
199
198
  if stages:
200
199
  errors.extend(_check_each_stage_section(text, stages))
201
200
  errors.extend(_check_depends_on(stages))
201
+ return errors
202
+
203
+
204
+ def main(argv: List[str]) -> int:
205
+ p = argparse.ArgumentParser()
206
+ p.add_argument("--plan", required=True)
207
+ args = p.parse_args(argv)
208
+ text = Path(args.plan).read_text(encoding="utf-8")
202
209
 
210
+ errors = collect_validation_errors(text)
203
211
  if errors:
204
212
  for e in errors:
205
213
  print(f"{e.code} stage={e.stage}: {e.message}", file=sys.stderr)
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import importlib.util
6
7
  import json
7
8
  import os
8
9
  import re
@@ -878,6 +879,7 @@ FINAL_VERIFICATION_REQUIRED_SECTIONS = (
878
879
  "Residual Risk",
879
880
  "Validation Evidence",
880
881
  "Read-only Command Log",
882
+ "Conditional Acceptance Conditions",
881
883
  "Routing Recommendation",
882
884
  )
883
885
 
@@ -1057,6 +1059,48 @@ def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
1057
1059
  for err in errors:
1058
1060
  failures.append(f"final-report data.json: {err}")
1059
1061
 
1062
+ if (data.get("header") or {}).get("taskType") == "final-verification":
1063
+ _validate_final_verification_consistency(data, failures)
1064
+
1065
+
1066
+ def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
1067
+ """Enforce verdict ↔ blocker/condition/routing consistency on the
1068
+ final-verification data.json (SSOT). The schema guarantees field SHAPE;
1069
+ these are the cross-field invariants the release-handoff gate depends on.
1070
+
1071
+ No-op for non-final-verification data so the caller's gate stays defensive.
1072
+ """
1073
+ if (data.get("header") or {}).get("taskType") != "final-verification":
1074
+ return
1075
+ verdict = data.get("finalVerdict") or {}
1076
+ token = (verdict.get("verdictToken") or "").strip().lower()
1077
+ fv = data.get("finalVerification") or {}
1078
+ blockers = fv.get("acceptanceBlockers") or []
1079
+ conditions = verdict.get("conditionalAcceptanceConditions") or []
1080
+ routing = fv.get("routingRecommendation") or ""
1081
+
1082
+ if token == "accepted" and blockers:
1083
+ failures.append(
1084
+ "final-verification: verdict `accepted` but acceptanceBlockers is "
1085
+ "non-empty — an accepted verdict must have zero blockers."
1086
+ )
1087
+ if token == "blocked" and not blockers:
1088
+ failures.append(
1089
+ "final-verification: verdict `blocked` but acceptanceBlockers is "
1090
+ "empty — a blocked verdict must list at least one blocker."
1091
+ )
1092
+ if token == "conditional-accept" and not conditions:
1093
+ failures.append(
1094
+ "final-verification: verdict `conditional-accept` but "
1095
+ "conditionalAcceptanceConditions is empty — list every condition."
1096
+ )
1097
+ if "release-handoff" in routing and token != "accepted":
1098
+ failures.append(
1099
+ f"final-verification: routingRecommendation cites `release-handoff` "
1100
+ f"but verdict is `{token}` — release-handoff routing is allowed only "
1101
+ "when the verdict is `accepted`."
1102
+ )
1103
+
1060
1104
 
1061
1105
  def validate_report_views(report_path: Path, failures: list[str]) -> None:
1062
1106
  """Enforce Phase 7 step 1.5 (BLOCKING) — the self-contained HTML
@@ -1095,6 +1139,44 @@ def validate_report_views(report_path: Path, failures: list[str]) -> None:
1095
1139
  failures.append(f"report-views: {line}")
1096
1140
 
1097
1141
 
1142
+ _STAGE_VALIDATOR_PATH = _VALIDATORS_DIR / "validate-implementation-plan-stages.py"
1143
+
1144
+
1145
+ def _load_stage_validator():
1146
+ spec = importlib.util.spec_from_file_location(
1147
+ "_ip_stage_validator", _STAGE_VALIDATOR_PATH
1148
+ )
1149
+ if spec is None or spec.loader is None:
1150
+ return None
1151
+ mod = importlib.util.module_from_spec(spec)
1152
+ # Register before exec so the dataclass field-type resolution can find the
1153
+ # module in sys.modules (mirrors run.py._parse_stage_map_into_ctx).
1154
+ sys.modules["_ip_stage_validator"] = mod
1155
+ try:
1156
+ spec.loader.exec_module(mod)
1157
+ finally:
1158
+ sys.modules.pop("_ip_stage_validator", None)
1159
+ return mod
1160
+
1161
+
1162
+ def _append_stage_structure_failures(content: str, failures: list[str]) -> None:
1163
+ """Enforce the Stage Map structural contract at the implementation-planning
1164
+ boundary. Without this, a plan missing `## 4.5 Stage Map` passes the
1165
+ planning gate, gets approved, and only fails later at the `implementation`
1166
+ entry (validators/validate-implementation-plan-stages.py via
1167
+ prepare_task_bundle). Running the same validator here moves the failure to
1168
+ produce-time."""
1169
+ mod = _load_stage_validator()
1170
+ if mod is None: # pragma: no cover — repo/runtime always ship the file
1171
+ failures.append(f"cannot load Stage Map validator at {_STAGE_VALIDATOR_PATH}")
1172
+ return
1173
+ for e in mod.collect_validation_errors(content):
1174
+ failures.append(
1175
+ f"implementation-planning Stage Map structure invalid "
1176
+ f"[{e.code} stage={e.stage}]: {e.message}"
1177
+ )
1178
+
1179
+
1098
1180
  def validate_phase_boundary(
1099
1181
  task_type: str,
1100
1182
  report_path: Path,
@@ -1210,6 +1292,12 @@ def validate_phase_boundary(
1210
1292
  "must NOT publish a pre-approved plan when verification did not pass."
1211
1293
  )
1212
1294
 
1295
+ # Only a publishable plan (gate passed) can be flipped to `approved: true`
1296
+ # and reach the `implementation` entry, so the Stage Map structure is
1297
+ # enforced only here — a blocked/aborted plan may legitimately be incomplete.
1298
+ if gate_value in ("passed", "passed-with-dissent"):
1299
+ _append_stage_structure_failures(content, failures)
1300
+
1213
1301
 
1214
1302
  def _parse_brief_frontmatter(brief_path: Path) -> dict:
1215
1303
  """Parse YAML frontmatter from a brief file into a flat dict.
@@ -1286,6 +1374,29 @@ def _validate_improvement_discovery(
1286
1374
  failures.append(f"improvement-discovery: {err}")
1287
1375
 
1288
1376
 
1377
+ def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
1378
+ """requirements-discovery run 에 fan-out/ 이 있으면 packet+index 를 검증해
1379
+ 실패를 ``requirements-discovery: `` 접두로 folding 한다. fan-out 이 없으면 no-op.
1380
+ """
1381
+ from pathlib import Path as _Path
1382
+ if not (_Path(run_dir) / "fan-out").is_dir():
1383
+ return
1384
+ _validators_dir = _Path(__file__).resolve().parent
1385
+ if str(_validators_dir) not in sys.path:
1386
+ sys.path.insert(0, str(_validators_dir))
1387
+ try:
1388
+ from validate_fanout import validate_fanout # noqa: E402
1389
+ except Exception as exc: # pragma: no cover - import guard
1390
+ failures.append(
1391
+ f"requirements-discovery: validate_fanout import failed — {exc}"
1392
+ )
1393
+ return
1394
+ result = validate_fanout(_Path(run_dir))
1395
+ if not result.ok:
1396
+ for err in result.errors:
1397
+ failures.append(f"requirements-discovery: {err}")
1398
+
1399
+
1289
1400
  def _refresh_task_catalog(project_root: Path, task_manifest: dict) -> tuple[bool, str]:
1290
1401
  """Regenerate `discovery/task-catalog.json` so it stops trailing the
1291
1402
  authoritative `task-manifest.json` after validation.
@@ -1634,6 +1745,9 @@ def main() -> int:
1634
1745
  )
1635
1746
  run_dir = report_path.parent.parent
1636
1747
  _validate_improvement_discovery(report_path, run_dir, brief_path, failures)
1748
+ if task_type == "requirements-discovery":
1749
+ run_dir = report_path.parent.parent
1750
+ _validate_requirements_discovery_fanout(run_dir, failures)
1637
1751
  validate_report_views(report_path, failures)
1638
1752
 
1639
1753
  validation_status = "passed" if not failures else "failed"
@@ -259,10 +259,10 @@ def validate(path: Path) -> list[str]:
259
259
  if section_name not in section_positions:
260
260
  continue
261
261
  start = section_positions[section_name]
262
- # find next `## ` heading or end of file
263
- next_h = re.search(r"^##\s", "\n".join(lines[start + 1:]), re.MULTILINE)
264
- end = (start + 1 + (next_h.start() if next_h else len(text))) if next_h else len(lines)
265
- section_text = "\n".join(lines[start:end])
262
+ # slice from this heading to the next `## ` (string offsets, not line idx)
263
+ rest = "\n".join(lines[start + 1:])
264
+ next_h = re.search(r"^##\s", rest, re.MULTILINE)
265
+ section_text = rest[: next_h.start()] if next_h else rest
266
266
  if not any(sub in section_text for sub in expected_substrings):
267
267
  violations.append(message)
268
268