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.
- package/README.kr.md +1 -1
- package/README.md +1 -1
- package/docs/kr/architecture.md +18 -2
- package/docs/kr/cli.md +1 -1
- package/docs/project-structure-overview.md +2 -3
- package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +326 -0
- package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +337 -0
- package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +410 -0
- package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +728 -0
- package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +113 -0
- package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +173 -0
- package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +154 -0
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +3 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/{python → bin}/lib/okstra/usage.sh +3 -2
- package/runtime/bin/okstra-codex-exec.sh +3 -3
- package/runtime/bin/okstra-trace-cleanup.sh +64 -26
- package/runtime/prompts/profiles/_common-contract.md +9 -5
- package/runtime/prompts/profiles/final-verification.md +18 -16
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/requirements-discovery.md +18 -1
- package/runtime/prompts/wizard/prompts.ko.json +11 -0
- package/runtime/python/okstra_ctl/consumers.py +1 -1
- package/runtime/python/okstra_ctl/fanout.py +35 -0
- package/runtime/python/okstra_ctl/migrate.py +21 -42
- package/runtime/python/okstra_ctl/reconcile.py +2 -2
- package/runtime/python/okstra_ctl/render_final_report.py +0 -1
- package/runtime/python/okstra_ctl/run.py +0 -29
- package/runtime/python/okstra_ctl/run_context.py +9 -12
- package/runtime/python/okstra_ctl/seeding.py +0 -192
- package/runtime/python/okstra_ctl/wizard.py +70 -5
- package/runtime/python/okstra_ctl/work_categories.py +21 -0
- package/runtime/python/okstra_ctl/worktree.py +74 -77
- package/runtime/python/okstra_project/__init__.py +0 -6
- package/runtime/python/okstra_project/dirs.py +0 -8
- package/runtime/schemas/final-report-v1.0.schema.json +34 -27
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +2 -0
- package/runtime/templates/prd/brief.template.md +1 -1
- package/runtime/templates/reports/fan-out-unit.template.md +25 -0
- package/runtime/templates/reports/final-report.template.md +24 -13
- package/runtime/templates/reports/final-verification-input.template.md +16 -5
- package/runtime/templates/reports/i18n/en.json +6 -3
- package/runtime/templates/reports/i18n/ko.json +6 -3
- package/runtime/templates/worker-prompt-preamble.md +7 -0
- package/runtime/validators/lib/fixtures.sh +2 -2
- package/runtime/validators/lib/validate-assets.sh +9 -0
- package/runtime/validators/validate-implementation-plan-stages.py +19 -11
- package/runtime/validators/validate-run.py +114 -0
- package/runtime/validators/validate-schedule.py +4 -4
- package/runtime/validators/validate_fanout.py +99 -0
- package/src/_proc.mjs +31 -0
- package/src/check-project.mjs +1 -25
- package/src/config.mjs +7 -31
- package/src/doctor.mjs +10 -29
- package/src/install.mjs +8 -36
- package/src/migrate.mjs +1 -18
- package/src/okstra-dirs.mjs +0 -11
- package/src/setup.mjs +1 -154
- package/src/uninstall.mjs +6 -13
- package/runtime/templates/okstra.CLAUDE.md +0 -104
- /package/runtime/{python → bin}/lib/okstra/cli.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/globals.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/interactive.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra/project-resolver.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-batch.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-list.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-open.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-projects.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reconcile.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reindex.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-rerun.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-show.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/cmd-tail.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/main.sh +0 -0
- /package/runtime/{python → bin}/lib/okstra-ctl/prepare.sh +0 -0
- /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", "
|
|
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
|
-
"
|
|
554
|
-
"
|
|
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", "
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
82
|
-
2.
|
|
83
|
-
3.
|
|
84
|
-
4.
|
|
85
|
-
5.
|
|
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
|
|
120
|
+
"commitListSummary": "Cited commit list",
|
|
121
|
+
"diffSummaryQuote": "Cited diff summary (from implementation report)",
|
|
121
122
|
"targetWorktreePath": "Target worktree path for verification",
|
|
122
|
-
"
|
|
123
|
-
"
|
|
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
|
|
120
|
+
"commitListSummary": "인용된 commit list",
|
|
121
|
+
"diffSummaryQuote": "인용된 diff summary (implementation 보고서)",
|
|
121
122
|
"targetWorktreePath": "검증 대상 worktree path",
|
|
122
|
-
"
|
|
123
|
-
"
|
|
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 /
|
|
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
|
|
185
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
section_text =
|
|
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
|
|