okstra 0.141.3 → 0.143.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 (51) hide show
  1. package/docs/architecture.md +11 -2
  2. package/docs/cli.md +15 -0
  3. package/docs/for-ai/skills/okstra-setup.md +8 -0
  4. package/docs/project-structure-overview.md +6 -0
  5. package/docs/task-process/error-analysis.md +9 -4
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/workers/report-writer-worker.md +4 -2
  9. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +3 -3
  10. package/runtime/prompts/coding-preflight/overview.md +1 -1
  11. package/runtime/prompts/lead/adapters/claude-code.md +2 -2
  12. package/runtime/prompts/lead/context-loader.md +2 -2
  13. package/runtime/prompts/lead/convergence.md +5 -2
  14. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  15. package/runtime/prompts/lead/plan-body-verification.md +20 -9
  16. package/runtime/prompts/lead/report-writer.md +4 -3
  17. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -0
  18. package/runtime/prompts/profiles/_common-contract.md +3 -1
  19. package/runtime/prompts/profiles/_implementation-deliverable.md +3 -3
  20. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  21. package/runtime/prompts/profiles/_implementation-verifier.md +2 -1
  22. package/runtime/prompts/profiles/error-analysis.md +5 -1
  23. package/runtime/prompts/profiles/forbidden-actions.json +0 -1
  24. package/runtime/prompts/profiles/implementation-planning.md +7 -2
  25. package/runtime/prompts/profiles/requirements-discovery.md +7 -0
  26. package/runtime/python/okstra_ctl/analysis_packet.py +28 -3
  27. package/runtime/python/okstra_ctl/brief_frontmatter.py +56 -0
  28. package/runtime/python/okstra_ctl/clarification_items.py +99 -5
  29. package/runtime/python/okstra_ctl/convergence_engine.py +66 -15
  30. package/runtime/python/okstra_ctl/paths.py +34 -7
  31. package/runtime/python/okstra_ctl/phase_cleanup.py +235 -0
  32. package/runtime/python/okstra_ctl/plan_items.py +38 -0
  33. package/runtime/python/okstra_ctl/run.py +81 -33
  34. package/runtime/python/okstra_ctl/schema_excerpt.py +5 -3
  35. package/runtime/python/okstra_ctl/wizard.py +18 -44
  36. package/runtime/python/okstra_ctl/worker_heartbeat.py +15 -5
  37. package/runtime/python/okstra_ctl/workflow.py +1 -1
  38. package/runtime/python/okstra_project/resolver.py +25 -0
  39. package/runtime/schemas/final-report-v1.0.schema.json +162 -4
  40. package/runtime/skills/okstra-run/SKILL.md +3 -1
  41. package/runtime/skills/okstra-setup/SKILL.md +3 -0
  42. package/runtime/skills/okstra-setup/references/project-config.md +47 -0
  43. package/runtime/templates/reports/final-report.template.md +51 -0
  44. package/runtime/templates/reports/i18n/en.json +32 -3
  45. package/runtime/templates/reports/i18n/ko.json +32 -3
  46. package/runtime/templates/reports/implementation-input.template.md +1 -2
  47. package/runtime/templates/reports/task-brief.template.md +1 -1
  48. package/runtime/validators/validate-brief.py +5 -1
  49. package/runtime/validators/validate-run.py +430 -48
  50. package/src/cli-registry.mjs +10 -0
  51. package/src/commands/execute/phase-cleanup.mjs +38 -0
@@ -33,17 +33,27 @@ SINGLE_WRITE_STAGES = frozenset({
33
33
  })
34
34
  SINGLE_WRITE_MAX_GAP_SECONDS = 20 * 60 + 60
35
35
 
36
+ # report-writer 가 도구 호출 없이 모든 입력을 정독(required-reading-complete)하거나
37
+ # 종합(synthesis-start)하는 구간. 그 사이에는 `in-stage:` 라인조차 append 할 수 없어
38
+ # 5분 예산으로는 살아 있는 워커가 stalled 로 판정된다 (실측: synthesis-start gap 41건
39
+ # 중 24%가 5분+60s 초과, 최대 14.9분). 단일 Write 만큼 길지는 않으므로 15분 예산.
40
+ SYNTHESIS_STAGES = frozenset({
41
+ "required-reading-complete",
42
+ "synthesis-start",
43
+ })
44
+ SYNTHESIS_MAX_GAP_SECONDS = 15 * 60 + 60
45
+
36
46
 
37
47
  def max_gap_seconds_after(stage: str) -> int:
38
48
  """*stage* 를 알린 뒤 다음 하트비트까지 허용되는 최대 공백(초).
39
49
 
40
50
  간격이 재는 것은 직전에 선언된 단계의 작업 시간이므로, 예산은 언제나
41
51
  구간을 *여는* 단계에서 고른다."""
42
- return (
43
- SINGLE_WRITE_MAX_GAP_SECONDS
44
- if stage in SINGLE_WRITE_STAGES
45
- else HEARTBEAT_MAX_GAP_SECONDS
46
- )
52
+ if stage in SINGLE_WRITE_STAGES:
53
+ return SINGLE_WRITE_MAX_GAP_SECONDS
54
+ if stage in SYNTHESIS_STAGES:
55
+ return SYNTHESIS_MAX_GAP_SECONDS
56
+ return HEARTBEAT_MAX_GAP_SECONDS
47
57
 
48
58
 
49
59
  @dataclass(frozen=True)
@@ -87,7 +87,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
87
87
  ' - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
88
88
  " - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
89
89
  " - per-verifier sections for every verifier in the resolved roster (`Claude verifier`, `Codex verifier`, plus `Antigravity verifier` when opted in) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
90
- " - rollback verification (revert SHA reachable, feature flag toggle works, migration down step valid; dry-run preferred)\n"
90
+ " - rollback verification (advisory, never blocks — record the revert path for a human; `result` is ok / not-applicable / advisory — human-run)\n"
91
91
  " - routing recommendation for `final-verification` (ready / needs new error-analysis or planning loop)"
92
92
  ),
93
93
  },
@@ -83,6 +83,31 @@ def resolve_project_root(*, explicit_root: str = "",
83
83
  "프로젝트 루트 또는 그 하위에서 실행하거나, git 작업 트리 안에서 실행해 주십시오.)")
84
84
 
85
85
 
86
+ _ARCHITECTURE_STYLES = frozenset({"hexagonal", "layered", "none"})
87
+
88
+
89
+ def resolve_architecture(project_root: Path | str) -> str:
90
+ """Return the declared architecture style, else ``"none"``.
91
+
92
+ Mirrors resolve_build_tool_tokens: any read/parse failure or an
93
+ unrecognised value falls back to the neutral ``"none"`` so an
94
+ unconfigured project keeps the layer-1 behaviour only.
95
+ """
96
+ try:
97
+ payload = json.loads(project_json_path(Path(project_root)).read_text(encoding="utf-8"))
98
+ # ValueError covers json.JSONDecodeError and UnicodeDecodeError alike — a
99
+ # project.json hand-saved in a non-UTF-8 encoding must fall back, not raise.
100
+ except (OSError, ValueError):
101
+ return "none"
102
+ if not isinstance(payload, dict):
103
+ return "none"
104
+ architecture = payload.get("architecture")
105
+ style = architecture.get("style") if isinstance(architecture, dict) else None
106
+ if not isinstance(style, str):
107
+ return "none"
108
+ return style if style in _ARCHITECTURE_STYLES else "none"
109
+
110
+
86
111
  def upsert_project_json(project_root: Path, project_id: str, *,
87
112
  now: Optional[str] = None) -> dict:
88
113
  """project.json 을 읽거나 새로 만든다.
@@ -384,6 +384,8 @@
384
384
  "items": { "$ref": "#/$defs/RiskRow" }
385
385
  },
386
386
 
387
+ "errorAnalysis": { "$ref": "#/$defs/ErrorAnalysis" },
388
+
387
389
  "implementationPlanning": {
388
390
  "type": "object",
389
391
  "description": "RENDER_IF taskType == implementation-planning. §5.5 deliverables.",
@@ -400,7 +402,8 @@
400
402
  "planBodyVerification",
401
403
  "crossProjectDependencies",
402
404
  "decisionDrafts",
403
- "skippedAdrCandidates"
405
+ "skippedAdrCandidates",
406
+ "variationPointAnalysis"
404
407
  ],
405
408
  "additionalProperties": false,
406
409
  "properties": {
@@ -426,6 +429,7 @@
426
429
  "items": { "$ref": "#/$defs/ImplementationPlanStage" }
427
430
  },
428
431
  "designPreparation": { "$ref": "#/$defs/DesignPreparation" },
432
+ "variationPointAnalysis": { "$ref": "#/$defs/VariationPointAnalysis" },
429
433
  "stepwiseExecution": {
430
434
  "description": "Legacy flat summary kept for compatibility only. New reports use stageMap/stages.",
431
435
  "type": "array",
@@ -812,6 +816,16 @@
812
816
  },
813
817
 
814
818
  "allOf": [
819
+ {
820
+ "description": "error-analysis task-type requires a structured diagnosis block.",
821
+ "if": {
822
+ "properties": { "header": { "properties": { "taskType": { "const": "error-analysis" } } } },
823
+ "required": ["header"]
824
+ },
825
+ "then": {
826
+ "required": ["errorAnalysis"]
827
+ }
828
+ },
815
829
  {
816
830
  "description": "implementation-planning task-type requires §5.5 block.",
817
831
  "if": {
@@ -950,6 +964,7 @@
950
964
  "Direction": {
951
965
  "enum": [
952
966
  "continue-investigation",
967
+ "begin-planning",
953
968
  "begin-implementation",
954
969
  "approve",
955
970
  "reject",
@@ -957,6 +972,92 @@
957
972
  ]
958
973
  },
959
974
 
975
+ "ErrorAnalysis": {
976
+ "type": "object",
977
+ "additionalProperties": false,
978
+ "required": [
979
+ "symptomVerbatim",
980
+ "observableFailure",
981
+ "reproduction",
982
+ "causeCandidates",
983
+ "nextDiagnostic",
984
+ "routing"
985
+ ],
986
+ "properties": {
987
+ "symptomVerbatim": { "type": "string" },
988
+ "observableFailure": { "type": "string" },
989
+ "reproduction": {
990
+ "type": "object",
991
+ "additionalProperties": false,
992
+ "required": ["status", "evidence", "blockedReason"],
993
+ "properties": {
994
+ "status": {
995
+ "enum": ["reproduced", "not-reproduced", "blocked-before-repro"]
996
+ },
997
+ "evidence": {
998
+ "type": "array",
999
+ "minItems": 1,
1000
+ "items": { "type": "string" }
1001
+ },
1002
+ "blockedReason": { "type": "string" }
1003
+ }
1004
+ },
1005
+ "causeCandidates": {
1006
+ "type": "array",
1007
+ "items": {
1008
+ "type": "object",
1009
+ "additionalProperties": false,
1010
+ "required": [
1011
+ "id",
1012
+ "statement",
1013
+ "supportingEvidence",
1014
+ "falsifyingEvidenceChecked",
1015
+ "confidence",
1016
+ "disproveWith"
1017
+ ],
1018
+ "properties": {
1019
+ "id": { "type": "string", "pattern": "^EA-\\d{3,}$" },
1020
+ "statement": { "type": "string" },
1021
+ "supportingEvidence": {
1022
+ "type": "array",
1023
+ "minItems": 1,
1024
+ "items": { "type": "string" }
1025
+ },
1026
+ "falsifyingEvidenceChecked": {
1027
+ "type": "array",
1028
+ "minItems": 1,
1029
+ "items": { "type": "string" }
1030
+ },
1031
+ "confidence": { "enum": ["low", "medium", "high"] },
1032
+ "disproveWith": { "type": "string", "minLength": 1 }
1033
+ }
1034
+ }
1035
+ },
1036
+ "nextDiagnostic": {
1037
+ "type": "object",
1038
+ "additionalProperties": false,
1039
+ "required": ["action", "confirmingSignal", "rejectingSignal"],
1040
+ "properties": {
1041
+ "action": { "type": "string", "minLength": 1 },
1042
+ "confirmingSignal": { "type": "string", "minLength": 1 },
1043
+ "rejectingSignal": { "type": "string", "minLength": 1 }
1044
+ }
1045
+ },
1046
+ "routing": {
1047
+ "type": "object",
1048
+ "additionalProperties": false,
1049
+ "required": ["nextTaskType", "leadingCauseId", "rationale"],
1050
+ "properties": {
1051
+ "nextTaskType": {
1052
+ "enum": ["error-analysis", "implementation-planning"]
1053
+ },
1054
+ "leadingCauseId": { "type": "string" },
1055
+ "rationale": { "type": "string", "minLength": 1 }
1056
+ }
1057
+ }
1058
+ }
1059
+ },
1060
+
960
1061
  "TicketId": {
961
1062
  "type": "string",
962
1063
  "minLength": 1,
@@ -1294,13 +1395,70 @@
1294
1395
 
1295
1396
  "RecommendedOption": {
1296
1397
  "type": "object",
1297
- "required": ["name", "coreReason", "rationale", "rejectedSummary"],
1398
+ "required": ["name", "coreReason", "rationale", "rejectedSummary", "testSeams"],
1298
1399
  "additionalProperties": false,
1299
1400
  "properties": {
1300
1401
  "name": { "type": "string", "minLength": 1 },
1301
1402
  "coreReason": { "type": "string", "minLength": 1 },
1302
1403
  "rationale": { "type": "string", "minLength": 1 },
1303
- "rejectedSummary": { "type": "string", "minLength": 1 }
1404
+ "rejectedSummary": { "type": "string", "minLength": 1 },
1405
+ "testSeams": {
1406
+ "type": "array",
1407
+ "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).",
1408
+ "items": {
1409
+ "type": "object",
1410
+ "required": ["boundary", "injectedAs", "replacedInTest"],
1411
+ "additionalProperties": false,
1412
+ "properties": {
1413
+ "boundary": { "type": "string", "minLength": 1 },
1414
+ "injectedAs": { "type": "string", "minLength": 1 },
1415
+ "replacedInTest": { "type": "string", "minLength": 1 }
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+ },
1421
+
1422
+ "VariationPointAnalysis": {
1423
+ "type": "object",
1424
+ "required": ["hasMultipleImplementations", "noVariationRationale", "points"],
1425
+ "additionalProperties": false,
1426
+ "properties": {
1427
+ "hasMultipleImplementations": { "type": "boolean" },
1428
+ "noVariationRationale": {
1429
+ "type": "string",
1430
+ "description": "Why no variation point exists when hasMultipleImplementations is false. Empty string when it is true."
1431
+ },
1432
+ "points": {
1433
+ "type": "array",
1434
+ "items": { "$ref": "#/$defs/VariationPoint" }
1435
+ }
1436
+ }
1437
+ },
1438
+
1439
+ "VariationPoint": {
1440
+ "type": "object",
1441
+ "required": ["behavior", "implementations", "evidence", "extractionDecision"],
1442
+ "additionalProperties": false,
1443
+ "properties": {
1444
+ "behavior": { "type": "string", "minLength": 1 },
1445
+ "implementations": {
1446
+ "type": "array",
1447
+ "minItems": 2,
1448
+ "items": { "type": "string", "minLength": 1 }
1449
+ },
1450
+ "evidence": { "type": "string", "minLength": 1 },
1451
+ "extractionDecision": {
1452
+ "type": "object",
1453
+ "required": ["extract", "interfaceKind", "coveredBy", "rationale"],
1454
+ "additionalProperties": false,
1455
+ "properties": {
1456
+ "extract": { "type": "boolean" },
1457
+ "interfaceKind": { "type": "string" },
1458
+ "coveredBy": { "type": "string" },
1459
+ "rationale": { "type": "string", "minLength": 1 }
1460
+ }
1461
+ }
1304
1462
  }
1305
1463
  },
1306
1464
 
@@ -2157,7 +2315,7 @@
2157
2315
  "rollbackCommand": { "type": "string", "minLength": 1 },
2158
2316
  "verification": { "type": "string", "minLength": 1 },
2159
2317
  "result": {
2160
- "enum": ["ok", "unable — route back to planning"]
2318
+ "enum": ["ok", "not-applicable", "advisory — human-run"]
2161
2319
  }
2162
2320
  }
2163
2321
  },
@@ -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, move to the next stage. Clean up context at each stage boundary (leftover panes·finished teammates from the previous batch).
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.
@@ -171,6 +171,32 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
171
171
 
172
172
  {% endif %}
173
173
 
174
+ {% if header.taskType == 'error-analysis' %}
175
+ ### 2.4 Error Analysis Result{% if t("errorAnalysis.heading") != "Error Analysis Result" %} ({{ t("errorAnalysis.heading") }}){% endif %}
176
+
177
+ - **{{ t("errorAnalysis.symptomVerbatim") }}:** {{ errorAnalysis.symptomVerbatim }}
178
+ - **{{ t("errorAnalysis.observableFailure") }}:** {{ errorAnalysis.observableFailure }}
179
+ - **{{ t("errorAnalysis.reproductionStatus") }}:** `{{ errorAnalysis.reproduction.status }}`
180
+ - **{{ t("errorAnalysis.reproductionEvidence") }}:** {{ errorAnalysis.reproduction.evidence | join(", ") }}
181
+ {% if errorAnalysis.reproduction.blockedReason %}- **{{ t("errorAnalysis.blockedReason") }}:** {{ errorAnalysis.reproduction.blockedReason }}
182
+ {% endif %}
183
+
184
+ {% if errorAnalysis.causeCandidates | length == 0 -%}
185
+ {{ t("emptyState.errorAnalysisCauseCandidates") }}
186
+ {%- else %}
187
+ | ID | {{ t("errorAnalysis.candidate") }} | {{ t("errorAnalysis.supportingEvidence") }} | {{ t("errorAnalysis.falsifyingEvidence") }} | {{ t("errorAnalysis.confidence") }} | {{ t("errorAnalysis.disproveWith") }} |
188
+ |---|---|---|---|---|---|
189
+ {% for row in errorAnalysis.causeCandidates -%}
190
+ | {{ row.id | mdcell }} | {{ row.statement | mdcell }} | {{ row.supportingEvidence | join(", ") | mdcell }} | {{ row.falsifyingEvidenceChecked | join(", ") | mdcell }} | `{{ row.confidence | mdcell }}` | {{ row.disproveWith | mdcell }} |
191
+ {% endfor %}
192
+ {%- endif %}
193
+
194
+ - **{{ t("errorAnalysis.nextDiagnostic") }}:** {{ errorAnalysis.nextDiagnostic.action }}
195
+ - **{{ t("errorAnalysis.confirmingSignal") }}:** {{ errorAnalysis.nextDiagnostic.confirmingSignal }}
196
+ - **{{ t("errorAnalysis.rejectingSignal") }}:** {{ errorAnalysis.nextDiagnostic.rejectingSignal }}
197
+ - **{{ t("errorAnalysis.route") }}:** `{{ errorAnalysis.routing.nextTaskType }}`{% if errorAnalysis.routing.leadingCauseId %} — `{{ errorAnalysis.routing.leadingCauseId }}`{% endif %} — {{ errorAnalysis.routing.rationale }}
198
+ {% endif %}
199
+
174
200
  ## 3. Recommended Next Steps
175
201
 
176
202
  {% if recommendedNextSteps | length == 0 -%}
@@ -477,6 +503,31 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
477
503
  {% endfor %}
478
504
  {%- endif %}
479
505
 
506
+ ### 5.5.11 Variation-Point Analysis{% if t("implementationPlanning.variationPointAnalysis.heading") != "Variation-Point Analysis" %} ({{ t("implementationPlanning.variationPointAnalysis.heading") }}){% endif %}
507
+
508
+ - **{{ t("implementationPlanning.variationPointAnalysis.hasMultiple") }}:** `{% if implementationPlanning.variationPointAnalysis.hasMultipleImplementations %}yes{% else %}no{% endif %}`
509
+
510
+ {% if not implementationPlanning.variationPointAnalysis.hasMultipleImplementations -%}
511
+ - **{{ t("implementationPlanning.variationPointAnalysis.rationale") }}:** {{ implementationPlanning.variationPointAnalysis.noVariationRationale }}
512
+ {% else %}
513
+ {% for point in implementationPlanning.variationPointAnalysis["points"] %}
514
+ - **{{ t("implementationPlanning.variationPointAnalysis.behavior") }}:** {{ point.behavior }}
515
+ - **{{ t("implementationPlanning.variationPointAnalysis.implementations") }}:** {{ point.implementations | join(", ") }}
516
+ - **{{ t("implementationPlanning.variationPointAnalysis.evidence") }}:** {{ point.evidence }}
517
+ - **{{ t("implementationPlanning.variationPointAnalysis.extract") }}:** `{% if point.extractionDecision.extract %}yes{% else %}no{% endif %}` — {{ point.extractionDecision.interfaceKind }} ({{ point.extractionDecision.coveredBy }}) — {{ point.extractionDecision.rationale }}
518
+ {% endfor %}
519
+ {% endif %}
520
+
521
+ **{{ t("implementationPlanning.variationPointAnalysis.testSeams") }}:**
522
+
523
+ {% if implementationPlanning.recommendedOption["testSeams"] | length == 0 -%}
524
+ {{ t("implementationPlanning.variationPointAnalysis.seamEmpty") }}
525
+ {% else %}
526
+ {% for seam in implementationPlanning.recommendedOption["testSeams"] -%}
527
+ - {{ seam.boundary }} — {{ seam.injectedAs }} → {{ seam.replacedInTest }}
528
+ {% endfor %}
529
+ {% endif %}
530
+
480
531
  {% endif %}
481
532
  {% if header.taskType == 'release-handoff' %}
482
533
  ## 5.6 Release Handoff Deliverables
@@ -19,7 +19,8 @@
19
19
  "lingeringRisks": "- No tracked lingering risks.",
20
20
  "noClarification": "- No additional information requested. The Section 7 verdict stands as-is.",
21
21
  "noFollowUp": "- No follow-up tasks. The next phase for this run is in §3 (Recommended Next Steps).",
22
- "endStateCoverage": "No end-state coverage recorded for this phase."
22
+ "endStateCoverage": "No end-state coverage recorded for this phase.",
23
+ "errorAnalysisCauseCandidates": "No evidence-backed cause candidate yet."
23
24
  },
24
25
  "columns": {
25
26
  "recordMeta": "Record",
@@ -97,8 +98,25 @@
97
98
  "columnSections": "Sections",
98
99
  "columnRelatedIds": "Related item IDs"
99
100
  },
101
+ "errorAnalysis": {
102
+ "heading": "Error Analysis Result",
103
+ "symptomVerbatim": "Symptom Verbatim",
104
+ "observableFailure": "Observable Failure",
105
+ "reproductionStatus": "Reproduction Status",
106
+ "reproductionEvidence": "Reproduction Evidence",
107
+ "blockedReason": "Blocked Reason",
108
+ "candidate": "Cause Candidate",
109
+ "supportingEvidence": "Supporting Evidence",
110
+ "falsifyingEvidence": "Falsifying Evidence",
111
+ "confidence": "Confidence",
112
+ "disproveWith": "Disprove With",
113
+ "nextDiagnostic": "Next Diagnostic",
114
+ "confirmingSignal": "Confirming Signal",
115
+ "rejectingSignal": "Rejecting Signal",
116
+ "route": "Route"
117
+ },
100
118
  "finalVerdict": {
101
- "intro": "This run's final conclusion and next action. **`Direction`** is the recommended next action — one of `continue-investigation`, `begin-implementation`, `approve`, `reject`, or `hold` — and is present for every task-type. **`Verdict Token`** is meaningful only for the `final-verification` task-type, where it is one of `accepted`, `conditional-accept`, or `blocked` and serves as the `release-handoff` entry gate. For every other task-type, `Verdict Token` is always `not-applicable`."
119
+ "intro": "This run's final conclusion and next action. **`Direction`** is the recommended next action — one of `continue-investigation`, `begin-planning`, `begin-implementation`, `approve`, `reject`, or `hold` — and is present for every task-type. `begin-planning` means the diagnosis is ready to enter `implementation-planning`. **`Verdict Token`** is meaningful only for the `final-verification` task-type, where it is one of `accepted`, `conditional-accept`, or `blocked` and serves as the `release-handoff` entry gate. For every other task-type, `Verdict Token` is always `not-applicable`."
102
120
  },
103
121
  "evidence": {
104
122
  "sourceItemsColumnNote": "The `Source items` column is described in §6.1."
@@ -112,7 +130,7 @@
112
130
  "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
131
  "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
132
  "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.",
133
+ "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
134
  "planBodySourceLabel": "source §",
117
135
  "planBodyBlockerLabel": "blocks approval →",
118
136
  "optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
@@ -168,6 +186,17 @@
168
186
  "notApplicableReason": "Not applicable reason",
169
187
  "none": "(none)",
170
188
  "empty": "No implementation design preparation items."
189
+ },
190
+ "variationPointAnalysis": {
191
+ "heading": "Variation-Point Analysis",
192
+ "hasMultiple": "Multiple implementations",
193
+ "rationale": "No-variation rationale",
194
+ "behavior": "Behavior",
195
+ "implementations": "Implementations",
196
+ "evidence": "Evidence",
197
+ "extract": "Extract as interface",
198
+ "testSeams": "Test seams",
199
+ "seamEmpty": "No seams declared."
171
200
  }
172
201
  },
173
202
  "releaseHandoff": {
@@ -19,7 +19,8 @@
19
19
  "lingeringRisks": "- 추적 대상 잔존 위험 없음.",
20
20
  "noClarification": "- 추가 정보 요청 없음. Section 7 의 최종 판단이 그대로 유효합니다.",
21
21
  "noFollowUp": "- 후속 작업 없음. 본 run 의 다음 phase 는 §3 (Recommended Next Steps) 참고.",
22
- "endStateCoverage": "이번 phase 에 기록된 종료 상태 처리가 없습니다."
22
+ "endStateCoverage": "이번 phase 에 기록된 종료 상태 처리가 없습니다.",
23
+ "errorAnalysisCauseCandidates": "근거가 뒷받침하는 원인 후보가 아직 없습니다."
23
24
  },
24
25
  "columns": {
25
26
  "recordMeta": "항목",
@@ -97,8 +98,25 @@
97
98
  "columnSections": "등장 섹션",
98
99
  "columnRelatedIds": "관련 항목 IDs"
99
100
  },
101
+ "errorAnalysis": {
102
+ "heading": "오류 분석 결과",
103
+ "symptomVerbatim": "증상 원문",
104
+ "observableFailure": "관측된 실패",
105
+ "reproductionStatus": "재현 상태",
106
+ "reproductionEvidence": "재현 근거",
107
+ "blockedReason": "차단 사유",
108
+ "candidate": "원인 후보",
109
+ "supportingEvidence": "지지 근거",
110
+ "falsifyingEvidence": "반증 확인",
111
+ "confidence": "신뢰도",
112
+ "disproveWith": "반증 방법",
113
+ "nextDiagnostic": "다음 진단",
114
+ "confirmingSignal": "확인 신호",
115
+ "rejectingSignal": "기각 신호",
116
+ "route": "라우팅"
117
+ },
100
118
  "finalVerdict": {
101
- "intro": "이 run 의 최종 결론과 다음 행동입니다. **`Direction`** 은 권장 다음 행동으로 `continue-investigation`(조사 계속) · `begin-implementation`(구현 시작) · `approve`(승인) · `reject`(반려) · `hold`(보류) 중 하나이며, 모든 task-type 에 존재합니다. **`Verdict Token`** 은 `final-verification` task-type 에서만 의미를 가집니다 — `accepted` · `conditional-accept` · `blocked` 중 하나로 `release-handoff` 진입 게이트로 쓰입니다. 그 외 task-type 에서 `Verdict Token` 은 항상 `not-applicable`(해당 없음) 입니다."
119
+ "intro": "이 run 의 최종 결론과 다음 행동입니다. **`Direction`** 은 권장 다음 행동으로 `continue-investigation`(조사 계속) · `begin-planning`(계획 시작) · `begin-implementation`(구현 시작) · `approve`(승인) · `reject`(반려) · `hold`(보류) 중 하나이며, 모든 task-type 에 존재합니다. `begin-planning`은 진단이 `implementation-planning`에 진입할 준비가 됐음을 뜻합니다. **`Verdict Token`** 은 `final-verification` task-type 에서만 의미를 가집니다 — `accepted` · `conditional-accept` · `blocked` 중 하나로 `release-handoff` 진입 게이트로 쓰입니다. 그 외 task-type 에서 `Verdict Token` 은 항상 `not-applicable`(해당 없음) 입니다."
102
120
  },
103
121
  "evidence": {
104
122
  "sourceItemsColumnNote": "`Source items` 열 설명은 §6.1 과 동일합니다."
@@ -112,7 +130,7 @@
112
130
  "planBodyGateLegend": "게이트 결과 값 뜻 — `passed`: 이견 없이 통과 · `passed-with-dissent`: 소수 워커의 반대가 남았으나 통과(반대가 다수였다면 승인 차단) · `blocked-by-disagreement`: 다수 반대로 승인 차단 · `aborted-non-result`: 검증 자체가 결과를 내지 못함.",
113
131
  "planBodyBlockedByLegend": "게이트를 막은 입력 — `majority-disagree`: 워커 다수 반대 · `coverage-gap`: Requirement Coverage 의 gap / blocked 행(워커 반대와는 무관) · `non-result`: 검증 자체가 결과를 내지 못함. 이 항목이 없으면 차단 원인이 없다는 뜻이다.",
114
132
  "planBodyVerdictLegend": "판정(Verdict) — **AGREE**: 적힌 대로 실행 가능하고 다른 항목과 내부적으로 일관됨 · **SUPPLEMENT**: 항목 자체는 타당하나 의존성·엣지케이스·전제조건이 누락됨 · **DISAGREE**: 결함이 있음(결함 유형 참조) · **verification-error**: 워커 검증이 결과를 내지 못함.",
115
- "planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음) · 자가수정 가능 여부(Fixability) — planner-fixable: 코드·계획·브리프만으로 수정 가능 · needs-user-input: 외부 결정 필요",
133
+ "planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 (참고용 — 롤백은 사람이 수행하므로 게이트를 막지 않음) · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음) · 자가수정 가능 여부(Fixability) — planner-fixable: 코드·계획·브리프만으로 수정 가능 · needs-user-input: 외부 결정 필요",
116
134
  "planBodySourceLabel": "출처 §",
117
135
  "planBodyBlockerLabel": "승인 차단 →",
118
136
  "optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
@@ -168,6 +186,17 @@
168
186
  "notApplicableReason": "해당 없음 이유",
169
187
  "none": "(없음)",
170
188
  "empty": "구현 설계 준비 항목이 없습니다."
189
+ },
190
+ "variationPointAnalysis": {
191
+ "heading": "변이점 분석",
192
+ "hasMultiple": "복수 구현",
193
+ "rationale": "변이점 없음 근거",
194
+ "behavior": "동작",
195
+ "implementations": "구현체",
196
+ "evidence": "근거",
197
+ "extract": "인터페이스로 추출",
198
+ "testSeams": "테스트 이음새",
199
+ "seamEmpty": "선언된 이음새가 없습니다."
171
200
  }
172
201
  },
173
202
  "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) Is the rollback path still valid after the changes?
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, TDD, and rollback commands must be executed and recorded with actual output?
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?
@@ -841,6 +841,10 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
841
841
 
842
842
 
843
843
  def find_briefs(root: Path) -> Iterable[Path]:
844
+ if root.is_file():
845
+ if root.suffix == ".md":
846
+ yield root
847
+ return
844
848
  yield from root.rglob("*.md")
845
849
 
846
850
 
@@ -849,7 +853,7 @@ def main(argv: list[str] | None = None) -> int:
849
853
  parser.add_argument(
850
854
  "briefs_dir",
851
855
  type=Path,
852
- help="Directory containing brief markdown files (recursed).",
856
+ help="Brief markdown file or directory containing brief files.",
853
857
  )
854
858
  parser.add_argument(
855
859
  "--briefs-root",