okstra 0.141.3 → 0.142.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/docs/architecture.md +11 -2
- package/docs/cli.md +15 -0
- package/docs/for-ai/skills/okstra-setup.md +8 -0
- package/docs/project-structure-overview.md +4 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +2 -1
- package/runtime/prompts/coding-preflight/architectures/hexagonal.md +3 -3
- package/runtime/prompts/coding-preflight/overview.md +1 -1
- package/runtime/prompts/lead/adapters/claude-code.md +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/plan-body-verification.md +20 -9
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -0
- package/runtime/prompts/profiles/_implementation-deliverable.md +3 -3
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -1
- package/runtime/prompts/profiles/forbidden-actions.json +0 -1
- package/runtime/prompts/profiles/implementation-planning.md +7 -2
- package/runtime/prompts/profiles/requirements-discovery.md +7 -0
- package/runtime/python/okstra_ctl/clarification_items.py +99 -5
- package/runtime/python/okstra_ctl/paths.py +34 -7
- package/runtime/python/okstra_ctl/phase_cleanup.py +235 -0
- package/runtime/python/okstra_ctl/plan_items.py +38 -0
- package/runtime/python/okstra_ctl/wizard.py +16 -0
- package/runtime/python/okstra_ctl/worker_heartbeat.py +15 -5
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_project/resolver.py +25 -0
- package/runtime/schemas/final-report-v1.0.schema.json +63 -4
- package/runtime/skills/okstra-run/SKILL.md +3 -1
- package/runtime/skills/okstra-setup/SKILL.md +3 -0
- package/runtime/skills/okstra-setup/references/project-config.md +47 -0
- package/runtime/templates/reports/final-report.template.md +25 -0
- package/runtime/templates/reports/i18n/en.json +12 -1
- package/runtime/templates/reports/i18n/ko.json +12 -1
- package/runtime/templates/reports/implementation-input.template.md +1 -2
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/validators/validate-run.py +143 -12
- package/src/cli-registry.mjs +10 -0
- package/src/commands/execute/phase-cleanup.mjs +38 -0
|
@@ -400,7 +400,8 @@
|
|
|
400
400
|
"planBodyVerification",
|
|
401
401
|
"crossProjectDependencies",
|
|
402
402
|
"decisionDrafts",
|
|
403
|
-
"skippedAdrCandidates"
|
|
403
|
+
"skippedAdrCandidates",
|
|
404
|
+
"variationPointAnalysis"
|
|
404
405
|
],
|
|
405
406
|
"additionalProperties": false,
|
|
406
407
|
"properties": {
|
|
@@ -426,6 +427,7 @@
|
|
|
426
427
|
"items": { "$ref": "#/$defs/ImplementationPlanStage" }
|
|
427
428
|
},
|
|
428
429
|
"designPreparation": { "$ref": "#/$defs/DesignPreparation" },
|
|
430
|
+
"variationPointAnalysis": { "$ref": "#/$defs/VariationPointAnalysis" },
|
|
429
431
|
"stepwiseExecution": {
|
|
430
432
|
"description": "Legacy flat summary kept for compatibility only. New reports use stageMap/stages.",
|
|
431
433
|
"type": "array",
|
|
@@ -1294,13 +1296,70 @@
|
|
|
1294
1296
|
|
|
1295
1297
|
"RecommendedOption": {
|
|
1296
1298
|
"type": "object",
|
|
1297
|
-
"required": ["name", "coreReason", "rationale", "rejectedSummary"],
|
|
1299
|
+
"required": ["name", "coreReason", "rationale", "rejectedSummary", "testSeams"],
|
|
1298
1300
|
"additionalProperties": false,
|
|
1299
1301
|
"properties": {
|
|
1300
1302
|
"name": { "type": "string", "minLength": 1 },
|
|
1301
1303
|
"coreReason": { "type": "string", "minLength": 1 },
|
|
1302
1304
|
"rationale": { "type": "string", "minLength": 1 },
|
|
1303
|
-
"rejectedSummary": { "type": "string", "minLength": 1 }
|
|
1305
|
+
"rejectedSummary": { "type": "string", "minLength": 1 },
|
|
1306
|
+
"testSeams": {
|
|
1307
|
+
"type": "array",
|
|
1308
|
+
"description": "Boundaries a test injects and replaces. An empty array is a deliberate declaration that no seam is needed (the plan body must justify it).",
|
|
1309
|
+
"items": {
|
|
1310
|
+
"type": "object",
|
|
1311
|
+
"required": ["boundary", "injectedAs", "replacedInTest"],
|
|
1312
|
+
"additionalProperties": false,
|
|
1313
|
+
"properties": {
|
|
1314
|
+
"boundary": { "type": "string", "minLength": 1 },
|
|
1315
|
+
"injectedAs": { "type": "string", "minLength": 1 },
|
|
1316
|
+
"replacedInTest": { "type": "string", "minLength": 1 }
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
},
|
|
1322
|
+
|
|
1323
|
+
"VariationPointAnalysis": {
|
|
1324
|
+
"type": "object",
|
|
1325
|
+
"required": ["hasMultipleImplementations", "noVariationRationale", "points"],
|
|
1326
|
+
"additionalProperties": false,
|
|
1327
|
+
"properties": {
|
|
1328
|
+
"hasMultipleImplementations": { "type": "boolean" },
|
|
1329
|
+
"noVariationRationale": {
|
|
1330
|
+
"type": "string",
|
|
1331
|
+
"description": "Why no variation point exists when hasMultipleImplementations is false. Empty string when it is true."
|
|
1332
|
+
},
|
|
1333
|
+
"points": {
|
|
1334
|
+
"type": "array",
|
|
1335
|
+
"items": { "$ref": "#/$defs/VariationPoint" }
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1340
|
+
"VariationPoint": {
|
|
1341
|
+
"type": "object",
|
|
1342
|
+
"required": ["behavior", "implementations", "evidence", "extractionDecision"],
|
|
1343
|
+
"additionalProperties": false,
|
|
1344
|
+
"properties": {
|
|
1345
|
+
"behavior": { "type": "string", "minLength": 1 },
|
|
1346
|
+
"implementations": {
|
|
1347
|
+
"type": "array",
|
|
1348
|
+
"minItems": 2,
|
|
1349
|
+
"items": { "type": "string", "minLength": 1 }
|
|
1350
|
+
},
|
|
1351
|
+
"evidence": { "type": "string", "minLength": 1 },
|
|
1352
|
+
"extractionDecision": {
|
|
1353
|
+
"type": "object",
|
|
1354
|
+
"required": ["extract", "interfaceKind", "coveredBy", "rationale"],
|
|
1355
|
+
"additionalProperties": false,
|
|
1356
|
+
"properties": {
|
|
1357
|
+
"extract": { "type": "boolean" },
|
|
1358
|
+
"interfaceKind": { "type": "string" },
|
|
1359
|
+
"coveredBy": { "type": "string" },
|
|
1360
|
+
"rationale": { "type": "string", "minLength": 1 }
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1304
1363
|
}
|
|
1305
1364
|
},
|
|
1306
1365
|
|
|
@@ -2157,7 +2216,7 @@
|
|
|
2157
2216
|
"rollbackCommand": { "type": "string", "minLength": 1 },
|
|
2158
2217
|
"verification": { "type": "string", "minLength": 1 },
|
|
2159
2218
|
"result": {
|
|
2160
|
-
"enum": ["ok", "
|
|
2219
|
+
"enum": ["ok", "not-applicable", "advisory — human-run"]
|
|
2161
2220
|
}
|
|
2162
2221
|
}
|
|
2163
2222
|
},
|
|
@@ -161,6 +161,8 @@ okstra config set pr-template-path "<value>" --scope global
|
|
|
161
161
|
|
|
162
162
|
If an action has an unknown `command`, `key`, or `scope`, stop and report the wizard output instead of inventing a command.
|
|
163
163
|
|
|
164
|
+
Before rendering the next phase's bundle, reclaim the prior phase's residual resources: run `okstra phase-cleanup --task-key <args.project-id>:<args.task-group>:<args.task-id> --project-root "<PROJECT_ROOT>" --fallback-team "session-<lead.sessionId-prefix>"`, then send `SendMessage(to: <name>, message: { type: "shutdown_request" })` to each `dismissible-teammates` name — only to teammates you have confirmed complete, never the lead and never an in-flight worker. `--fallback-team` takes the same `session-<lead.sessionId-prefix>` label the lead recorded in team-state, and must always be passed: after a resume or compaction the session id is re-issued, and without the label the reconcile finds no live roster and prints no teammate to dismiss. Assemble `--task-key` as the 3-token `project-id:task-group:task-id` literal from `outcome.renderArgs`; any other shape (a `<task-group>/<task-id>` pair, a bare task-id) is rejected and the whole cleanup — teammate reconcile included — is silently skipped. **This applies only when advancing to a different analysis phase.** The implementation stage chain (Step 7) does its own per-stage reclaim in its step 3 and does NOT run this `--task-key` form. `--run-dir` is unnecessary — the command auto-discovers the newest completed run across phases, including staged (implementation stage / single-stage final-verification) `runs/<type>/stage-N/` runs; pass `--run-dir` only to override that discovery with a specific prior run. A cleanup failure never blocks the next phase (the Python worker returns 0); only a malformed invocation exits non-zero.
|
|
165
|
+
|
|
164
166
|
Build the `okstra render-bundle` invocation from `outcome.renderArgs`, passing each key as `--<key>` and the value verbatim (including empty strings — they are intentional `use phase default` markers).
|
|
165
167
|
|
|
166
168
|
Step 3's empty-answer and escaping rules apply verbatim: every flag whose value is the empty string MUST still be passed explicitly as `--<key> ""` (e.g. `--workers ""`, `--directive ""`) — `render-bundle` distinguishes "flag absent" from "flag present with empty value", and the wizard's intent is always the latter.
|
|
@@ -258,7 +260,7 @@ Queue = the topologically-sorted stage list from splitting `chain-stages` on `,`
|
|
|
258
260
|
|
|
259
261
|
1. Call Step 5's `render-bundle` with the same arguments but `--stage N` (the base commit is auto-computed by prepare from the predecessor's done `head_commit`, so do not pass it by hand). Step 5's blocking local conformance waiver offer·concurrent-run detection·git-reconcile gates apply identically to each stage's `render-bundle`.
|
|
260
262
|
2. As in Step 6, become Claude lead and run that stage's Phase 1–7 inline. Phase 6's lead post-stage persistence appends that stage's `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` (per the implementation profile directive).
|
|
261
|
-
3. After confirming that `done` row was written,
|
|
263
|
+
3. After confirming that `done` row was written, run `okstra phase-cleanup --run-dir "<the run dir of the stage you just completed>" --project-root "<PROJECT_ROOT>" --fallback-team "session-<lead.sessionId-prefix>"`; for each `dismissible-teammates` name, send `SendMessage(to: <name>, message: { type: "shutdown_request" })` — only to teammates you have confirmed complete, never the lead and never an in-flight worker. Then move to the next stage.
|
|
262
264
|
4. One-line report at each stage start/finish: `stage N/<total> start` / `stage N done → next K`.
|
|
263
265
|
|
|
264
266
|
Once the whole queue is consumed, end the chain and report completion to the user.
|
|
@@ -116,6 +116,9 @@ matching section:
|
|
|
116
116
|
offered on the first release-handoff run.
|
|
117
117
|
- **E. final report language** (`reportLanguage`) — default is auto (follows
|
|
118
118
|
the brief's language).
|
|
119
|
+
- **F. declared architecture style** (`architecture.style`) — `hexagonal` /
|
|
120
|
+
`layered` / `none`; hand-added, default `none`. Declaring one makes that
|
|
121
|
+
architecture's placement rules binding in planning + verification.
|
|
119
122
|
|
|
120
123
|
## Step 4: Verify
|
|
121
124
|
|
|
@@ -196,3 +196,50 @@ okstra config set report-language <en|ko|auto> --scope project
|
|
|
196
196
|
|
|
197
197
|
Set the global default manually with `--scope global` as described in the
|
|
198
198
|
README's "global config" guidance — this flow offers only project scope.
|
|
199
|
+
|
|
200
|
+
## F. Declared architecture style (`architecture.style`)
|
|
201
|
+
|
|
202
|
+
`architecture.style` declares the project's architecture — one of
|
|
203
|
+
`hexagonal`, `layered`, or `none`. It is optional and defaults to `none`: an
|
|
204
|
+
absent field, an unrecognized value, or an unreadable `project.json` all
|
|
205
|
+
resolve to `none`
|
|
206
|
+
(`scripts/okstra_project/resolver.py::resolve_architecture`), so a project
|
|
207
|
+
that never declares one keeps today's behaviour unchanged.
|
|
208
|
+
|
|
209
|
+
Like `worktreeSyncDirs` and `qaCommands`, `okstra setup` does NOT write this
|
|
210
|
+
field — hand-add it to `project.json`. It is preserved across the runtime's
|
|
211
|
+
auto-upserts (only `projectId`, `projectRoot`, `createdAt`, `updatedAt` are
|
|
212
|
+
runtime-owned), so the manual edit survives every subsequent `okstra setup` /
|
|
213
|
+
`okstra run` invocation.
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"projectId": "...",
|
|
218
|
+
"projectRoot": "...",
|
|
219
|
+
"architecture": { "style": "hexagonal" }
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Declaring `hexagonal` or `layered` promotes that architecture's placement
|
|
224
|
+
rules from advisory to a binding planning + verification constraint:
|
|
225
|
+
|
|
226
|
+
- `hexagonal` — extraction itself stays a free decision (`extract: false`
|
|
227
|
+
remains legal), but a variation point `implementation-planning` *does*
|
|
228
|
+
extract has to sit behind a port: an `extractionDecision` carrying
|
|
229
|
+
`extract: true` with any `interfaceKind` other than `"port"` fails
|
|
230
|
+
`validators/validate-run.py::_validate_variation_point_analysis`. The
|
|
231
|
+
implementation executor loads the `architectures/hexagonal.md` preflight
|
|
232
|
+
pack even when directory-shape detection did not match it, and the verifier
|
|
233
|
+
grades a service dependency the diff adds or modifies that injects a
|
|
234
|
+
concrete adapter instead of a port as a blocking `FAIL` rather than a
|
|
235
|
+
recommendation.
|
|
236
|
+
- `layered` — no preflight pack resource. Its binding invariant is dependency
|
|
237
|
+
direction: an upper layer may import a lower one, never the reverse. A
|
|
238
|
+
reverse import is a blocking placement violation found by worker judgement,
|
|
239
|
+
because no machine check reads layer names.
|
|
240
|
+
- `none` (or the field left out) — the style-agnostic planning rules still
|
|
241
|
+
run on every plan (variation-point analysis and test seams); only the
|
|
242
|
+
placement overlay stays advisory and detection-driven.
|
|
243
|
+
|
|
244
|
+
The full two-layer model lives in `docs/architecture.md` § Project
|
|
245
|
+
self-registration.
|
|
@@ -477,6 +477,31 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
477
477
|
{% endfor %}
|
|
478
478
|
{%- endif %}
|
|
479
479
|
|
|
480
|
+
### 5.5.11 Variation-Point Analysis{% if t("implementationPlanning.variationPointAnalysis.heading") != "Variation-Point Analysis" %} ({{ t("implementationPlanning.variationPointAnalysis.heading") }}){% endif %}
|
|
481
|
+
|
|
482
|
+
- **{{ t("implementationPlanning.variationPointAnalysis.hasMultiple") }}:** `{% if implementationPlanning.variationPointAnalysis.hasMultipleImplementations %}yes{% else %}no{% endif %}`
|
|
483
|
+
|
|
484
|
+
{% if not implementationPlanning.variationPointAnalysis.hasMultipleImplementations -%}
|
|
485
|
+
- **{{ t("implementationPlanning.variationPointAnalysis.rationale") }}:** {{ implementationPlanning.variationPointAnalysis.noVariationRationale }}
|
|
486
|
+
{% else %}
|
|
487
|
+
{% for point in implementationPlanning.variationPointAnalysis["points"] %}
|
|
488
|
+
- **{{ t("implementationPlanning.variationPointAnalysis.behavior") }}:** {{ point.behavior }}
|
|
489
|
+
- **{{ t("implementationPlanning.variationPointAnalysis.implementations") }}:** {{ point.implementations | join(", ") }}
|
|
490
|
+
- **{{ t("implementationPlanning.variationPointAnalysis.evidence") }}:** {{ point.evidence }}
|
|
491
|
+
- **{{ t("implementationPlanning.variationPointAnalysis.extract") }}:** `{% if point.extractionDecision.extract %}yes{% else %}no{% endif %}` — {{ point.extractionDecision.interfaceKind }} ({{ point.extractionDecision.coveredBy }}) — {{ point.extractionDecision.rationale }}
|
|
492
|
+
{% endfor %}
|
|
493
|
+
{% endif %}
|
|
494
|
+
|
|
495
|
+
**{{ t("implementationPlanning.variationPointAnalysis.testSeams") }}:**
|
|
496
|
+
|
|
497
|
+
{% if implementationPlanning.recommendedOption["testSeams"] | length == 0 -%}
|
|
498
|
+
{{ t("implementationPlanning.variationPointAnalysis.seamEmpty") }}
|
|
499
|
+
{% else %}
|
|
500
|
+
{% for seam in implementationPlanning.recommendedOption["testSeams"] -%}
|
|
501
|
+
- {{ seam.boundary }} — {{ seam.injectedAs }} → {{ seam.replacedInTest }}
|
|
502
|
+
{% endfor %}
|
|
503
|
+
{% endif %}
|
|
504
|
+
|
|
480
505
|
{% endif %}
|
|
481
506
|
{% if header.taskType == 'release-handoff' %}
|
|
482
507
|
## 5.6 Release Handoff Deliverables
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"planBodyGateLegend": "Gate values — `passed`: agreed, no dissent · `passed-with-dissent`: a minority dissent remains but the gate passes (a majority dissent would block approval) · `blocked-by-disagreement`: majority dissent blocks approval · `aborted-non-result`: verification itself produced no result.",
|
|
113
113
|
"planBodyBlockedByLegend": "Which input blocked the gate — `majority-disagree`: a worker majority dissent · `coverage-gap`: a Requirement Coverage gap / blocked row, independent of worker votes · `non-result`: verification produced no result. Absent means nothing blocked.",
|
|
114
114
|
"planBodyVerdictLegend": "Verdict — **AGREE**: executable as written and internally consistent with other items · **SUPPLEMENT**: item is sound but a dependency / edge case / precondition is missing · **DISAGREE**: has a defect (see Breakage kind) · **verification-error**: the worker produced no result.",
|
|
115
|
-
"planBodyBreakageLegend": "Breakage kind — a: cited file path/symbol mismatches another step or option · b: command is not executable or is ambiguous · c: validation signal is not observable · d: rollback violates commit/dependency order · e: contradicts the trade-off matrix · f: requirement-coverage row does not map to an option/stage/step that actually satisfies the requirement. (`--` = not applicable) Fixability: planner-fixable = correctable from code + plan + brief; needs-user-input = requires an external decision.",
|
|
115
|
+
"planBodyBreakageLegend": "Breakage kind — a: cited file path/symbol mismatches another step or option · b: command is not executable or is ambiguous · c: validation signal is not observable · d: rollback violates commit/dependency order (advisory — a rollback is human-run, so this never blocks the gate) · e: contradicts the trade-off matrix · f: requirement-coverage row does not map to an option/stage/step that actually satisfies the requirement. (`--` = not applicable) Fixability: planner-fixable = correctable from code + plan + brief; needs-user-input = requires an external decision.",
|
|
116
116
|
"planBodySourceLabel": "source §",
|
|
117
117
|
"planBodyBlockerLabel": "blocks approval →",
|
|
118
118
|
"optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
|
|
@@ -168,6 +168,17 @@
|
|
|
168
168
|
"notApplicableReason": "Not applicable reason",
|
|
169
169
|
"none": "(none)",
|
|
170
170
|
"empty": "No implementation design preparation items."
|
|
171
|
+
},
|
|
172
|
+
"variationPointAnalysis": {
|
|
173
|
+
"heading": "Variation-Point Analysis",
|
|
174
|
+
"hasMultiple": "Multiple implementations",
|
|
175
|
+
"rationale": "No-variation rationale",
|
|
176
|
+
"behavior": "Behavior",
|
|
177
|
+
"implementations": "Implementations",
|
|
178
|
+
"evidence": "Evidence",
|
|
179
|
+
"extract": "Extract as interface",
|
|
180
|
+
"testSeams": "Test seams",
|
|
181
|
+
"seamEmpty": "No seams declared."
|
|
171
182
|
}
|
|
172
183
|
},
|
|
173
184
|
"releaseHandoff": {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"planBodyGateLegend": "게이트 결과 값 뜻 — `passed`: 이견 없이 통과 · `passed-with-dissent`: 소수 워커의 반대가 남았으나 통과(반대가 다수였다면 승인 차단) · `blocked-by-disagreement`: 다수 반대로 승인 차단 · `aborted-non-result`: 검증 자체가 결과를 내지 못함.",
|
|
113
113
|
"planBodyBlockedByLegend": "게이트를 막은 입력 — `majority-disagree`: 워커 다수 반대 · `coverage-gap`: Requirement Coverage 의 gap / blocked 행(워커 반대와는 무관) · `non-result`: 검증 자체가 결과를 내지 못함. 이 항목이 없으면 차단 원인이 없다는 뜻이다.",
|
|
114
114
|
"planBodyVerdictLegend": "판정(Verdict) — **AGREE**: 적힌 대로 실행 가능하고 다른 항목과 내부적으로 일관됨 · **SUPPLEMENT**: 항목 자체는 타당하나 의존성·엣지케이스·전제조건이 누락됨 · **DISAGREE**: 결함이 있음(결함 유형 참조) · **verification-error**: 워커 검증이 결과를 내지 못함.",
|
|
115
|
-
"planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음) · 자가수정 가능 여부(Fixability) — planner-fixable: 코드·계획·브리프만으로 수정 가능 · needs-user-input: 외부 결정 필요",
|
|
115
|
+
"planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 (참고용 — 롤백은 사람이 수행하므로 게이트를 막지 않음) · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음) · 자가수정 가능 여부(Fixability) — planner-fixable: 코드·계획·브리프만으로 수정 가능 · needs-user-input: 외부 결정 필요",
|
|
116
116
|
"planBodySourceLabel": "출처 §",
|
|
117
117
|
"planBodyBlockerLabel": "승인 차단 →",
|
|
118
118
|
"optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
|
|
@@ -168,6 +168,17 @@
|
|
|
168
168
|
"notApplicableReason": "해당 없음 이유",
|
|
169
169
|
"none": "(없음)",
|
|
170
170
|
"empty": "구현 설계 준비 항목이 없습니다."
|
|
171
|
+
},
|
|
172
|
+
"variationPointAnalysis": {
|
|
173
|
+
"heading": "변이점 분석",
|
|
174
|
+
"hasMultiple": "복수 구현",
|
|
175
|
+
"rationale": "변이점 없음 근거",
|
|
176
|
+
"behavior": "동작",
|
|
177
|
+
"implementations": "구현체",
|
|
178
|
+
"evidence": "근거",
|
|
179
|
+
"extract": "인터페이스로 추출",
|
|
180
|
+
"testSeams": "테스트 이음새",
|
|
181
|
+
"seamEmpty": "선언된 이음새가 없습니다."
|
|
171
182
|
}
|
|
172
183
|
},
|
|
173
184
|
"releaseHandoff": {
|
|
@@ -80,8 +80,7 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
80
80
|
2. (Executor) Did any change touch a file or symbol that appears in `Out of Scope`? If yes, halt and report — do not silently include it.
|
|
81
81
|
3. (Verifiers) Does the diff match the plan's File Structure and step ordering?
|
|
82
82
|
4. (Verifiers) Does the validation evidence include actual command output and exit codes for every plan checkpoint?
|
|
83
|
-
5. (Verifiers)
|
|
84
|
-
6. (Verifiers) Does the diff contain any "while I'm here" edits (rename, reformat, comment cleanup, adjacent refactor) that the plan did not authorise? Flag each such hunk explicitly.
|
|
83
|
+
5. (Verifiers) Does the diff contain any "while I'm here" edits (rename, reformat, comment cleanup, adjacent refactor) that the plan did not authorise? Flag each such hunk explicitly.
|
|
85
84
|
|
|
86
85
|
## Phase Boundary
|
|
87
86
|
|
|
@@ -67,7 +67,7 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
67
67
|
- If `Task Type` is `implementation`:
|
|
68
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
|
-
- Which validation
|
|
70
|
+
- Which validation and TDD commands must be executed and recorded with actual output? (Rollback is human-run — record the revert path for reference; do not require executing it.)
|
|
71
71
|
- If `Task Type` is `final-verification`:
|
|
72
72
|
- What was delivered?
|
|
73
73
|
- What acceptance criteria must pass?
|
|
@@ -34,6 +34,7 @@ except ImportError: # pragma: no cover — runtime guarantees this import
|
|
|
34
34
|
|
|
35
35
|
from okstra_project import project_json_path # noqa: E402
|
|
36
36
|
from okstra_project.dirs import tasks_root as _okstra_tasks_root # noqa: E402
|
|
37
|
+
from okstra_project.resolver import resolve_architecture # noqa: E402
|
|
37
38
|
|
|
38
39
|
from okstra_ctl.conformance import ( # noqa: E402
|
|
39
40
|
CAPABILITY_WHITELIST,
|
|
@@ -2343,6 +2344,11 @@ def validate_final_report_data(
|
|
|
2343
2344
|
_validate_round_recorded_verdicts(data, failures)
|
|
2344
2345
|
_validate_verdicts_match_current_subjects(data, failures)
|
|
2345
2346
|
_validate_plan_item_extraction_completeness(data, failures)
|
|
2347
|
+
_validate_variation_point_analysis(
|
|
2348
|
+
(data.get("implementationPlanning") or {}).get("variationPointAnalysis"),
|
|
2349
|
+
resolve_architecture(_project_root_from_report(report_path)),
|
|
2350
|
+
failures,
|
|
2351
|
+
)
|
|
2346
2352
|
_validate_plan_item_subject_substance(data, failures)
|
|
2347
2353
|
_validate_plan_body_clarification_matching(data, failures)
|
|
2348
2354
|
_validate_disagree_has_fixability(data, failures)
|
|
@@ -2524,10 +2530,16 @@ _PLAN_GATE_RANK = {
|
|
|
2524
2530
|
|
|
2525
2531
|
# Breakage kinds where a single DISAGREE blocks the gate on its own (no majority
|
|
2526
2532
|
# needed), because the defect is concrete, safety-critical, and adversarially
|
|
2527
|
-
# verifiable: `a` = cited path/symbol mismatch
|
|
2528
|
-
#
|
|
2529
|
-
#
|
|
2530
|
-
_SINGLE_VOTE_BLOCKING_KINDS = {"a"
|
|
2533
|
+
# verifiable: `a` = cited path/symbol mismatch. `b`/`c`/`e` still need a
|
|
2534
|
+
# majority — `b` in particular is prone to planning-vs-implementation
|
|
2535
|
+
# environment false positives.
|
|
2536
|
+
_SINGLE_VOTE_BLOCKING_KINDS = {"a"}
|
|
2537
|
+
|
|
2538
|
+
# Rollback ordering (`d`) is executed by a human, not by okstra's workers or
|
|
2539
|
+
# verifiers, so a rollback-ordering dissent is recorded but never gates
|
|
2540
|
+
# approval: it is dropped from the blocking-disagree tally entirely, so an item
|
|
2541
|
+
# whose only DISAGREEs are advisory-only can never rise above `has-dissent`.
|
|
2542
|
+
_ADVISORY_ONLY_KINDS = {"d"}
|
|
2531
2543
|
|
|
2532
2544
|
# Stop reasons that justify promoting a still-broken planner-fixable item to the
|
|
2533
2545
|
# user: the self-fix budget ran out, or a round produced no net resolution so
|
|
@@ -2535,6 +2547,20 @@ _SINGLE_VOTE_BLOCKING_KINDS = {"a", "d"}
|
|
|
2535
2547
|
_SELF_FIX_EXHAUSTED_REASONS = frozenset({"max-rounds-reached", "no-progress"})
|
|
2536
2548
|
|
|
2537
2549
|
|
|
2550
|
+
def _is_variation_point_item(item: dict) -> bool:
|
|
2551
|
+
"""Whether this is a `P-Var-*` variation-point item, which is majority-gated
|
|
2552
|
+
(`prompts/lead/plan-body-verification.md` "`P-Var-<N>` … is majority-gated").
|
|
2553
|
+
Whether a behavior has two implementations, and whether the plan extracted the
|
|
2554
|
+
right interface for it, is a design judgement — it lacks the concrete certainty
|
|
2555
|
+
of kind `a`, where a verifier points at two spelled-out references that
|
|
2556
|
+
contradict each other. So kind `a` carries no extra weight on a P-Var item: it
|
|
2557
|
+
neither single-vote-blocks nor counts as correctness-critical, exactly like the
|
|
2558
|
+
`b` / `c` / `e` kinds the prompt routes P-Var defects to. Only a
|
|
2559
|
+
`majority-disagree` gates it — that part is unchanged.
|
|
2560
|
+
"""
|
|
2561
|
+
return str(item.get("id") or "").upper().startswith("P-VAR")
|
|
2562
|
+
|
|
2563
|
+
|
|
2538
2564
|
def _classify_plan_item_gate(item: dict) -> str:
|
|
2539
2565
|
"""Recompute one plan item's gate class from its per-worker verdicts,
|
|
2540
2566
|
per `prompts/lead/plan-body-verification.md` "Round protocol". Returns one of
|
|
@@ -2556,15 +2582,32 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
2556
2582
|
return "all-non-result"
|
|
2557
2583
|
disagree = [(vd, bk) for (vd, bk) in non_error if vd == "DISAGREE"]
|
|
2558
2584
|
agree = [(vd, bk) for (vd, bk) in non_error if vd in ("AGREE", "SUPPLEMENT")]
|
|
2559
|
-
disagree_kinds = {bk for (_vd, bk) in disagree if bk}
|
|
2560
2585
|
if not disagree:
|
|
2561
2586
|
return "full-consensus"
|
|
2587
|
+
# Rollback is a human-run operation, so rollback dissent never blocks the
|
|
2588
|
+
# gate — closed from two angles so a verifier cannot re-block it by relabelling:
|
|
2589
|
+
# (1) a whole rollback plan item (`P-Rb-*`) is advisory regardless of
|
|
2590
|
+
# breakage kind — otherwise a `DISAGREE(b)` "rollback command is
|
|
2591
|
+
# ambiguous" would sail past the kind-`d` exemption and block;
|
|
2592
|
+
# (2) a rollback-ordering dissent (`d`) is advisory on ANY item, since a
|
|
2593
|
+
# rollback-order defect raised against a non-rollback item is still a
|
|
2594
|
+
# human-run concern.
|
|
2595
|
+
# Both are recorded as dissent and fold into `has-dissent`, never blocking.
|
|
2596
|
+
if str(item.get("id") or "").upper().startswith("P-RB"):
|
|
2597
|
+
return "has-dissent"
|
|
2598
|
+
blocking_disagree = [(vd, bk) for (vd, bk) in disagree if bk not in _ADVISORY_ONLY_KINDS]
|
|
2599
|
+
if not blocking_disagree:
|
|
2600
|
+
return "has-dissent"
|
|
2601
|
+
blocking_kinds = {bk for (_vd, bk) in blocking_disagree if bk}
|
|
2562
2602
|
# Single-vote-blocking kinds: one confirmed DISAGREE on a concrete,
|
|
2563
2603
|
# safety-critical, adversarially-verifiable defect is enough to block, even
|
|
2564
2604
|
# in a two-worker roster — a lone correct dissent must not be outvoted here.
|
|
2565
|
-
# `a
|
|
2605
|
+
# `a` for any item except `P-Var-*` (majority-gated, see
|
|
2606
|
+
# `_is_variation_point_item`); `f` only for P-Req items (requirement coverage).
|
|
2566
2607
|
is_req = str(item.get("id") or "").upper().startswith("P-REQ")
|
|
2567
|
-
if
|
|
2608
|
+
if not _is_variation_point_item(item) and (
|
|
2609
|
+
blocking_kinds & _SINGLE_VOTE_BLOCKING_KINDS or (is_req and "f" in blocking_kinds)
|
|
2610
|
+
):
|
|
2568
2611
|
# "One confirmed DISAGREE" presupposes the item was actually
|
|
2569
2612
|
# cross-verified. When the peer returned a non-result nothing confirmed
|
|
2570
2613
|
# the dissent, so blocking here would reproduce the same
|
|
@@ -2577,7 +2620,7 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
2577
2620
|
# two participating votes, so a lone surviving DISAGREE (its peer returned a
|
|
2578
2621
|
# non-result) does NOT block. That fixes the paradox where a worker failure
|
|
2579
2622
|
# made the gate stricter than a healthy roster would.
|
|
2580
|
-
if len(non_error) >= 2 and len(
|
|
2623
|
+
if len(non_error) >= 2 and len(blocking_disagree) > len(agree):
|
|
2581
2624
|
return "majority-disagree"
|
|
2582
2625
|
return "has-dissent"
|
|
2583
2626
|
|
|
@@ -2604,11 +2647,18 @@ def _has_planner_fixable_majority(item: dict) -> bool:
|
|
|
2604
2647
|
|
|
2605
2648
|
def _is_correctness_critical(item: dict) -> bool:
|
|
2606
2649
|
"""Whether this item's defect would make `implementation` produce wrong or
|
|
2607
|
-
unsafe code — the single-vote-blocking
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
plan-prose defects: they degrade the document, not the
|
|
2650
|
+
unsafe code — the single-vote-blocking kind `a` (cited path/symbol mismatch)
|
|
2651
|
+
on any item but `P-Var-*`, or `f` (requirement-coverage mismatch) on a
|
|
2652
|
+
`P-Req-*` item.
|
|
2653
|
+
Kinds `b`/`c`/`e` are plan-prose defects: they degrade the document, not the
|
|
2654
|
+
resulting code. Rollback ordering (`d`) is advisory — a human runs the
|
|
2655
|
+
rollback — so it never counts as correctness-critical. A `P-Var-*` item is
|
|
2656
|
+
majority-gated end to end, so a kind-`a` dissent on one is no more critical
|
|
2657
|
+
than the `b`/`e` its defect should have been raised under; otherwise the same
|
|
2658
|
+
mis-tag that no longer single-vote-blocks would still veto the downgrade.
|
|
2611
2659
|
"""
|
|
2660
|
+
if _is_variation_point_item(item):
|
|
2661
|
+
return False
|
|
2612
2662
|
kinds = _disagree_breakage_kinds(item)
|
|
2613
2663
|
is_req = str(item.get("id") or "").upper().startswith("P-REQ")
|
|
2614
2664
|
return bool(kinds & _SINGLE_VOTE_BLOCKING_KINDS) or (is_req and "f" in kinds)
|
|
@@ -4131,6 +4181,87 @@ def _validate_plan_item_extraction_completeness(
|
|
|
4131
4181
|
)
|
|
4132
4182
|
|
|
4133
4183
|
|
|
4184
|
+
def _validate_variation_point_analysis(
|
|
4185
|
+
vpa: object,
|
|
4186
|
+
architecture_style: str,
|
|
4187
|
+
failures: list[str],
|
|
4188
|
+
) -> None:
|
|
4189
|
+
"""Conditional rules + architecture-style overlay for variation points.
|
|
4190
|
+
|
|
4191
|
+
The schema enforces shape only. Two layers of meaning sit on top:
|
|
4192
|
+
|
|
4193
|
+
Layer 1 is style-agnostic. Declaring "no variation exists" is a claim that
|
|
4194
|
+
needs a written reason, and it must not be paired with declared points —
|
|
4195
|
+
`plan_items._extract_variation_point_items` emits a lone `P-Var-0` in that
|
|
4196
|
+
branch and drops them, so the contradiction would silently exempt every
|
|
4197
|
+
declared point from per-point verification. `extract: true` is a claim in
|
|
4198
|
+
the same way: it names the interface the next implementation plugs into and
|
|
4199
|
+
the Stage Map stage that builds it, so both fields have to be filled. The
|
|
4200
|
+
schema cannot carry this as a `minLength` — the empty string is the natural
|
|
4201
|
+
shape of an `extract: false` decision.
|
|
4202
|
+
|
|
4203
|
+
Layer 2 fires only for a project that declares `architecture.style`
|
|
4204
|
+
`hexagonal`: extracting a variation point there means introducing a port,
|
|
4205
|
+
not a helper. An unconfigured project resolves to `none` and keeps layer-1
|
|
4206
|
+
behaviour only.
|
|
4207
|
+
"""
|
|
4208
|
+
if not isinstance(vpa, dict):
|
|
4209
|
+
failures.append("variationPointAnalysis is missing or not an object")
|
|
4210
|
+
return
|
|
4211
|
+
# Schema violations are reported, not raised, so this check still runs on a
|
|
4212
|
+
# malformed block. A type guard here keeps a bad field from aborting the
|
|
4213
|
+
# whole validation and discarding every failure collected so far.
|
|
4214
|
+
raw_points = vpa.get("points")
|
|
4215
|
+
if raw_points is not None and not isinstance(raw_points, list):
|
|
4216
|
+
failures.append("variationPointAnalysis: points must be an array")
|
|
4217
|
+
return
|
|
4218
|
+
points = raw_points or []
|
|
4219
|
+
if not bool(vpa.get("hasMultipleImplementations")):
|
|
4220
|
+
rationale = vpa.get("noVariationRationale")
|
|
4221
|
+
if not isinstance(rationale, str) or not rationale.strip():
|
|
4222
|
+
failures.append(
|
|
4223
|
+
"variationPointAnalysis: hasMultipleImplementations=false "
|
|
4224
|
+
"requires a non-empty noVariationRationale"
|
|
4225
|
+
)
|
|
4226
|
+
if points:
|
|
4227
|
+
failures.append(
|
|
4228
|
+
"variationPointAnalysis: hasMultipleImplementations=false but "
|
|
4229
|
+
"points is non-empty — declared variation points would be "
|
|
4230
|
+
"silently dropped"
|
|
4231
|
+
)
|
|
4232
|
+
return
|
|
4233
|
+
if not points:
|
|
4234
|
+
failures.append(
|
|
4235
|
+
"variationPointAnalysis: hasMultipleImplementations=true requires "
|
|
4236
|
+
"at least one point"
|
|
4237
|
+
)
|
|
4238
|
+
return
|
|
4239
|
+
for index, point in enumerate(points, start=1):
|
|
4240
|
+
if not isinstance(point, dict):
|
|
4241
|
+
failures.append(
|
|
4242
|
+
f"variationPointAnalysis point {index}: must be an object"
|
|
4243
|
+
)
|
|
4244
|
+
continue
|
|
4245
|
+
decision = point.get("extractionDecision")
|
|
4246
|
+
if not isinstance(decision, dict):
|
|
4247
|
+
continue # shape is the schema's job; don't double-report it.
|
|
4248
|
+
if not decision.get("extract"):
|
|
4249
|
+
continue
|
|
4250
|
+
for field in ("interfaceKind", "coveredBy"):
|
|
4251
|
+
value = decision.get(field)
|
|
4252
|
+
if not isinstance(value, str) or not value.strip():
|
|
4253
|
+
failures.append(
|
|
4254
|
+
f"variationPointAnalysis point {index}: extract=true "
|
|
4255
|
+
f"requires a non-empty {field}, got {value!r}"
|
|
4256
|
+
)
|
|
4257
|
+
if architecture_style == "hexagonal" and decision.get("interfaceKind") != "port":
|
|
4258
|
+
failures.append(
|
|
4259
|
+
f"variationPointAnalysis point {index}: architecture style "
|
|
4260
|
+
f"'hexagonal' requires interfaceKind 'port', got "
|
|
4261
|
+
f"{decision.get('interfaceKind')!r}"
|
|
4262
|
+
)
|
|
4263
|
+
|
|
4264
|
+
|
|
4134
4265
|
_DESIGN_PREP_CONTRACT = "implementation-design-prep-v1"
|
|
4135
4266
|
_DESIGN_PREP_REQUEST_STATUSES = {"provisional", "blocked"}
|
|
4136
4267
|
_DESIGN_PREP_TERMINAL_STATUSES = {"ready", "not-applicable"}
|
package/src/cli-registry.mjs
CHANGED
|
@@ -101,6 +101,16 @@ export const COMMAND_REGISTRY = [
|
|
|
101
101
|
category: "admin",
|
|
102
102
|
summary: ["stage 들을 task 브랜치에 통합 머지하고 stage worktree 를 정리"],
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
name: "phase-cleanup",
|
|
106
|
+
module: "./commands/execute/phase-cleanup.mjs",
|
|
107
|
+
export: "run",
|
|
108
|
+
category: "admin",
|
|
109
|
+
summary: [
|
|
110
|
+
"Reclaim the prior phase/batch's completed panes and list dismissible",
|
|
111
|
+
"teammates for the lead to shut down (tmux-aware)",
|
|
112
|
+
],
|
|
113
|
+
},
|
|
104
114
|
{
|
|
105
115
|
name: "paths",
|
|
106
116
|
module: "./commands/lifecycle/paths.mjs",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra phase-cleanup — reclaim the prior phase/batch's completed panes and teammates
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra phase-cleanup --project-root <dir> [--task-key <k>] [--run-dir <dir>]
|
|
7
|
+
[--fallback-team <label>] [--json]
|
|
8
|
+
|
|
9
|
+
--task-key project-id:task-group:task-id — finds the newest completed run
|
|
10
|
+
across phases, including staged (implementation / final-
|
|
11
|
+
verification) stage-N runs.
|
|
12
|
+
--run-dir explicit prior run dir (wins over --task-key).
|
|
13
|
+
--fallback-team
|
|
14
|
+
the live team label (session-<lead-session-prefix>) so teammate
|
|
15
|
+
reconcile still finds the roster after a session-id re-issue
|
|
16
|
+
(resume/compaction); omit and it resolves the current live
|
|
17
|
+
session only.
|
|
18
|
+
|
|
19
|
+
Reclaims only completed tmux panes (lead pane and in-flight workers are preserved),
|
|
20
|
+
and prints the dismissible-teammate names for the lead to shut down. In a non-tmux
|
|
21
|
+
session no panes exist, so it skips pane reclaim and only reconciles teammates.
|
|
22
|
+
A cleanup failure never blocks the next phase (the worker returns 0); a malformed
|
|
23
|
+
invocation still exits non-zero.
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export async function run(args) {
|
|
27
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
28
|
+
process.stdout.write(USAGE);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = await runPythonModule({
|
|
33
|
+
module: "okstra_ctl.phase_cleanup",
|
|
34
|
+
args,
|
|
35
|
+
stdio: "inherit-stdout",
|
|
36
|
+
});
|
|
37
|
+
return result.code;
|
|
38
|
+
}
|