okstra 0.71.2 → 0.73.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 (40) hide show
  1. package/docs/kr/architecture.md +16 -2
  2. package/docs/kr/cli.md +8 -2
  3. package/docs/kr/performance-improvement-plan-v2.md +27 -7
  4. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +1290 -0
  5. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +94 -0
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/SKILL.md +2 -2
  9. package/runtime/bin/lib/okstra/cli.sh +5 -1
  10. package/runtime/bin/lib/okstra/globals.sh +1 -0
  11. package/runtime/bin/lib/okstra/usage.sh +6 -1
  12. package/runtime/bin/okstra.sh +1 -0
  13. package/runtime/prompts/profiles/error-analysis.md +1 -1
  14. package/runtime/prompts/profiles/final-verification.md +1 -1
  15. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  16. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  17. package/runtime/prompts/wizard/prompts.ko.json +9 -0
  18. package/runtime/python/okstra_ctl/analysis_packet.py +17 -0
  19. package/runtime/python/okstra_ctl/clarification_items.py +26 -0
  20. package/runtime/python/okstra_ctl/context_cost.py +32 -5
  21. package/runtime/python/okstra_ctl/fix_cycles.py +172 -0
  22. package/runtime/python/okstra_ctl/render.py +28 -4
  23. package/runtime/python/okstra_ctl/report_views.py +63 -6
  24. package/runtime/python/okstra_ctl/run.py +97 -4
  25. package/runtime/python/okstra_ctl/run_context.py +15 -9
  26. package/runtime/python/okstra_ctl/wizard.py +64 -4
  27. package/runtime/python/okstra_token_usage/claude.py +22 -0
  28. package/runtime/python/okstra_token_usage/collect.py +46 -0
  29. package/runtime/python/okstra_token_usage/cursor.py +3 -1
  30. package/runtime/schemas/final-report-v1.0.schema.json +27 -1
  31. package/runtime/skills/okstra-brief/SKILL.md +8 -0
  32. package/runtime/skills/okstra-convergence/SKILL.md +8 -4
  33. package/runtime/skills/okstra-inspect/SKILL.md +20 -0
  34. package/runtime/skills/okstra-report-writer/SKILL.md +4 -2
  35. package/runtime/skills/okstra-run/SKILL.md +2 -1
  36. package/runtime/templates/project-docs/task-index.template.md +1 -0
  37. package/runtime/templates/reports/final-report.template.md +17 -3
  38. package/runtime/templates/reports/report.js +23 -2
  39. package/runtime/validators/validate-run.py +41 -0
  40. package/src/render-bundle.mjs +4 -1
@@ -18,7 +18,9 @@ from pathlib import Path
18
18
 
19
19
  from okstra_project.dirs import okstra_home
20
20
 
21
- CACHE_SCHEMA_VERSION = 1
21
+ # v2: usage 이벤트에 `p`(lead PROGRESS 마커) 키 추가 — v1 캐시는 이미 스캔한
22
+ # 구간의 마커가 없으므로 폐기하고 전체 재스캔한다(fail-open).
23
+ CACHE_SCHEMA_VERSION = 2
22
24
  IDENTITY_PREFIX_BYTES = 256
23
25
  MAX_NEEDLES = 16
24
26
 
@@ -625,6 +625,31 @@
625
625
  }
626
626
  },
627
627
 
628
+ "fixCycle": {
629
+ "type": "object",
630
+ "description": "RENDER_IF fixCycle present — the post-release bug-fix cycle this run belongs to.",
631
+ "required": ["cycle", "targetReport", "symptom"],
632
+ "additionalProperties": false,
633
+ "properties": {
634
+ "cycle": { "type": "string", "pattern": "^fc-[0-9]{2,}$" },
635
+ "targetReport": { "type": "string" },
636
+ "symptom": { "type": "string" },
637
+ "runs": {
638
+ "type": "array",
639
+ "items": {
640
+ "type": "object",
641
+ "required": ["taskType", "runSeq"],
642
+ "additionalProperties": false,
643
+ "properties": {
644
+ "taskType": { "type": "string" },
645
+ "runSeq": { "type": "integer" },
646
+ "runManifest": { "type": "string" }
647
+ }
648
+ }
649
+ }
650
+ }
651
+ },
652
+
628
653
  "clarificationItems": {
629
654
  "type": "array",
630
655
  "items": { "$ref": "#/$defs/ClarificationRow" }
@@ -1217,9 +1242,10 @@
1217
1242
 
1218
1243
  "ValidationCheckRow": {
1219
1244
  "type": "object",
1220
- "required": ["phase", "ticketId", "check", "commandOrObservation", "expectedOutcome"],
1245
+ "required": ["id", "phase", "ticketId", "check", "commandOrObservation", "expectedOutcome"],
1221
1246
  "additionalProperties": false,
1222
1247
  "properties": {
1248
+ "id": { "type": "string", "pattern": "^VC-\\d{3,}$" },
1223
1249
  "phase": { "enum": ["pre", "mid", "post"] },
1224
1250
  "ticketId": { "$ref": "#/$defs/TicketId" },
1225
1251
  "check": { "type": "string", "minLength": 1 },
@@ -431,6 +431,9 @@ never error:
431
431
  1. **okstra-internal (authoritative)** — always check first:
432
432
  - `<PROJECT_ROOT>/.okstra/glossary.md` if present
433
433
  - `<PROJECT_ROOT>/.okstra/decisions/` titles if present
434
+ - Fix history (when the brief targets an existing task):
435
+ `<PROJECT_ROOT>/.okstra/tasks/<task-group>/<task-id>/history/fix-cycles.jsonl`
436
+ — read every `opened`/`closed` row.
434
437
  2. **Explicit source material only** — if the reporter cited a path outside
435
438
  okstra's subtree, read it as source evidence only; do not treat it as
436
439
  okstra memory.
@@ -602,6 +605,11 @@ Required sections:
602
605
  - **Related Artifacts** — files, URLs, issues, prior task-keys.
603
606
  - **Open Questions** — anything the user already flagged as undecided
604
607
  (becomes raw material for `requirements-discovery`).
608
+ - **Task Continuity Notes** — if the target task's
609
+ `history/fix-cycles.jsonl` exists, cite each cycle here as one line —
610
+ `fix-cycle fc-NN (open|closed): <symptom> (target: <target_report>)`.
611
+ An open cycle means the new brief continues a bug-fix in progress; say
612
+ so explicitly.
605
613
 
606
614
  Sections **deliberately omitted** (do NOT add them, do NOT prompt for them):
607
615
 
@@ -519,14 +519,18 @@ Schema rules:
519
519
  Runs only when `convergence.critic.enabled == true` (set by `--critic <provider>` or the okstra-run `critic_pick` step; default off). Applies to the three finding-producing phases (`requirements-discovery`, `error-analysis`, `implementation-planning`); for `final-verification` the critic runs in a different mode — see §"Acceptance critic pass (final-verification)". This pass targets **coverage** (missed findings), distinct from convergence which targets **agreement quality**.
520
520
 
521
521
  ### When
522
- After Phase 5.5 finding convergence completes (findings classified) and BEFORE the Phase 6 report-writer dispatch.
522
+
523
+ The critic input is the Round 0 consolidated finding list. Reverify rounds only classify findings — they never add or remove them (in-round queue insertions are forbidden, see §"Convergence State Artifact" `carriedForwardCount`) — so the critic dispatch MUST NOT wait for classification to finish:
524
+
525
+ - **Dispatch**: immediately after Round 0 grouping, CONCURRENTLY with the first reverify round's dispatches. When the verification queue is empty after Round 0 (no reverify round runs), dispatch right after grouping. Concurrent dispatch to the same provider is safe — the critic result path (`<provider>-critic-...`) never collides with a reverify result path.
526
+ - **Gap verification + merge**: only after BOTH the finding-convergence loop has exited AND the critic result is collected, and BEFORE the Phase 6 report-writer dispatch. If the loop exited `aborted-non-result`, do NOT dispatch a gap-verification round — surface the gaps as unverified `clarification` items per §"Gap verification" and record that fact.
523
527
 
524
528
  ### Dispatch (reused worker)
525
529
  Dispatch ONE pass to the `config.critic.provider`'s existing subagent (`claude-worker` / `codex-worker` / `gemini-worker`) with `model = config.critic.modelExecutionValue` — no new agent type. If `config.critic.modelExecutionValue` is null/empty (model could not be resolved), skip the critic pass and record `critic-skipped: model-unresolved` in the convergence state rather than dispatching with no model. Result path: `runs/<task-type>/worker-results/<provider>-critic-<task-type>-<seq>.md`. The critic prompt seeds the consolidated findings and asks ONLY for coverage gaps:
526
530
 
527
531
  ```
528
- You are the coverage critic for <task-key>. Below are the findings the workers
529
- already agreed on. Your ONLY job is to name what is MISSING:
532
+ You are the coverage critic for <task-key>. Below are the consolidated findings
533
+ the workers produced. Your ONLY job is to name what is MISSING:
530
534
  - files / directories / execution paths nobody inspected,
531
535
  - requirements or acceptance points with zero findings,
532
536
  - claims raised but never verified.
@@ -543,7 +547,7 @@ Each critic gap enters the verification queue as a finding with `originWorker =
543
547
 
544
548
  ## Acceptance critic pass (final-verification)
545
549
 
546
- The `final-verification` phase reuses the SAME reused-worker dispatch as §"Coverage critic pass" (provider + `config.critic.modelExecutionValue` from the `convergence.critic` block; default off; same model-unresolved skip rule). Only the prompt, the verification semantics, and the output sink differ — final-verification's findings are defects/blockers, so the critic acts as an **acceptance devil's advocate** (find reasons NOT to accept), and its candidate blockers are NEVER dropped (that would suppress real defects).
550
+ The `final-verification` phase reuses the SAME reused-worker dispatch AND the same dispatch timing as §"Coverage critic pass" §"When" (provider + `config.critic.modelExecutionValue` from the `convergence.critic` block; default off; same model-unresolved skip rule) — the delivered work the critic inspects is likewise fixed before the reverify round starts. Only the prompt, the verification semantics, and the output sink differ — final-verification's findings are defects/blockers, so the critic acts as an **acceptance devil's advocate** (find reasons NOT to accept), and its candidate blockers are NEVER dropped (that would suppress real defects).
547
551
 
548
552
  ### Prompt
549
553
 
@@ -425,6 +425,15 @@ Display rule for `workerId` vs `agent`:
425
425
 
426
426
  Never write `claude (claude)` — the parenthesized agent is shown only when it adds information.
427
427
 
428
+ **C. Per-run phase breakdown (when `phaseTimeline` is present):**
429
+
430
+ team-state written by collector ≥ v0.73 carries a `phaseTimeline` block — the Phase 7 collector reconstructs lead operating-phase boundaries from the lead session's `PROGRESS: phase-*` checkpoint lines (`{source: "lead-progress-markers", phases: [{phase, firstAt, lastAt, markerCount, wallMsToNext}]}`).
431
+
432
+ - When the user asks for a per-phase / per-run breakdown ("단계별", "phase별", "어느 단계가 오래"), render one table per requested run: `| Phase | Start | Wall to next |` using `firstAt` and `wallMsToNext`.
433
+ - `wallMsToNext` is `null` on the last phase (the run-end signal is not a marker) — render `--`.
434
+ - `phases: []` means the lead emitted no markers (run predates the contract, or markers were skipped) — say "phase timeline 측정 불가(마커 없음)" rather than estimating from other fields.
435
+ - Older team-state files have no `phaseTimeline` key at all — omit the section silently.
436
+
428
437
  **Timestamp parsing:** for `startedAt` / `endedAt`, normalize ISO-8601: replace trailing `Z` with `+00:00`, accept explicit offsets as-is, parse via `datetime.fromisoformat(s.replace("Z", "+00:00"))`. Strings without an offset are assumed UTC. Mixed-form comparisons must be done as `datetime` objects, never raw strings.
429
438
 
430
439
  ### time.4 — Format output
@@ -458,6 +467,17 @@ Never write `claude (claude)` — the parenthesized agent is shown only when it
458
467
  | report-writer | 2 | 00:01:00 | 00:00:30 |
459
468
 
460
469
  > Unavailable: 1 run (implementation / 2026-04-30_03-03-48) — team-state has no durationMs (Phase 7 not reached)
470
+
471
+ ### Phase breakdown — requirements-discovery (run 002)
472
+
473
+ | Phase | Start (UTC) | Wall to next |
474
+ |------------------------|------------------|--------------|
475
+ | phase-1-intake | 2026-06-11 09:00 | 00:04:00 |
476
+ | phase-2-prompts | 2026-06-11 09:04 | 00:02:10 |
477
+ | phase-4-dispatch | 2026-06-11 09:06 | 00:18:30 |
478
+ | phase-5.5-convergence | 2026-06-11 09:25 | 00:21:00 |
479
+ | phase-6-synthesis | 2026-06-11 09:46 | 00:15:00 |
480
+ | phase-7-persist | 2026-06-11 10:01 | -- |
461
481
  ```
462
482
 
463
483
  **Rules:**
@@ -105,7 +105,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
105
105
  ```
106
106
 
107
107
  Output (idempotent — re-running overwrites):
108
- - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, **generated only when the report has at least one §5 `C-*` clarification row**. Those rows with `Status` ∈ {`open`, `answered`} embed form widgets (`<select>` for enum-style decisions, `<input>` for material / data-point kinds, `<textarea>` fallback); an `Export user response` button serialises form values to a markdown sidecar (schema in [`templates/reports/user-response.template.md`](../../templates/reports/user-response.template.md)) that the user pastes to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`. The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
108
+ - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, **generated only when the report has at least one §5 `C-*` clarification row**. Those rows with `Status` ∈ {`open`, `answered`} embed form widgets (`<select>` for enum-style decisions, `<input>` for material / data-point kinds, `<textarea>` fallback); an `Export user response` button serialises form values to a markdown sidecar (schema in [`templates/reports/user-response.template.md`](../../templates/reports/user-response.template.md)) and downloads it as `user-response-<task-type>-<seq>.md`; the user saves it to `runs/<task-type>/user-responses/`, and `--resume-clarification` auto-appends every sidecar found there to the next run's `clarification-response.md` (`clarification_items.clarification_response_with_sidecars`). The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
109
109
  - When the report has **no** `C-*` clarification rows, the html carries no interactive forms (it would only duplicate the MD), so the renderer prints `html: skipped (...)` and writes nothing. This is the expected state for clarification-free runs — `validators/validate-report-views.py` treats "no C-* rows + no html" as a pass, not a missing artifact.
110
110
 
111
111
  Must run AFTER step 1 (so token placeholders are substituted in any rendered html) and BEFORE step 2 (so the html artifact, when generated, exists for the validator step that checks it).
@@ -276,9 +276,11 @@ Section numbering follows `templates/reports/final-report.template.md` exactly
276
276
  3. **Evidence and Detailed Analysis** — primary evidence rows (file path, line, snippet); secondary evidence / alternate interpretations. If `reference-expectations.md` lists explicit expected values, record match/gap per row.
277
277
  4. **Missing Information and Risks** — uncertain / "I don't know" items. `implementation-planning` adds §5.5 (see heading contract below); `release-handoff` adds §5.6.
278
278
  5. **Clarification Items** — single unified `C-*` table; column schema (4 columns with the short fields stacked in one record-meta cell), ID convention, and rerun behaviour are owned by `_common-contract.md §Clarification request policy` (SSOT). The deprecated `5.5.9 Open Questions` / `1.1 추가 자료 요청` / `1.2 사용자 확인 질문` sub-sections are removed; the validator fails reports that reintroduce them.
279
- 6. **Recommended Next Steps** — prioritized actions. After Phase 7's follow-up spawner runs, append a row per newly created task-key (see "Phase 6 → Phase 7 execution sequence" above).
279
+ 6. **Recommended Next Steps** — prioritized actions. After Phase 7's follow-up spawner runs, append a row per newly created task-key (see "Phase 6 → Phase 7 execution sequence" above). **Approval-gate consistency:** when §1 carries any `Blocks: approval` row with `Status` ∈ {open, answered}, the Verdict Card `Next Step` and the first recommended step MUST point to the clarification rerun (`resume-clarification` of the SAME task-type) — never to "frontmatter `approved: true` 플립 → `implementation` 직행". Run-prep enforces this gate (`run.py _validate_approved_plan` fail-closes on those rows and on a blocking data.json `gateResult`), so a direct-implementation next-step is an instruction the reader cannot actually follow.
280
280
  7. **Follow-up Tasks** — auto-spawn-eligible table. Each row drives `okstra-spawn-followups.py`; see template §7 for the row schema.
281
281
 
282
+ **§5.10 Fix History (data-presence gated).** When the run-manifest carries a `fixCycleId`, fill the data.json `fixCycle` block (`cycle` / `targetReport` / `symptom` / `runs`). Read the values from the task root's `history/fix-cycles.jsonl`: `cycle` MUST equal `fixCycleId`, `targetReport` / `symptom` come from that cycle's `opened` row, and `runs` lists its attached `run` rows (`taskType` / `runSeq` / `runManifest`). The validator (`validators/validate-run.py` → `_validate_fix_cycle`) fails the run when the block is missing or `fixCycle.cycle` does not match `fixCycleId`. When the run-manifest has no `fixCycleId`, OMIT the `fixCycle` block entirely — the renderer omits §5.10.
283
+
282
284
  ### Writing Guidelines
283
285
 
284
286
  - Write in Markdown. **Prefer tables over prose bullet lists** for any section that enumerates multiple items with the same shape (evidence rows, risks, options, dependencies, rollback steps, follow-ups, open questions). Bullets are reserved for short, single-line standalone statements (e.g., "- 추가 정보 요청 없음."). When the template provides a table form, do NOT degrade it back to bullets in the rendered report.
@@ -178,7 +178,8 @@ okstra render-bundle \
178
178
  --report-writer-model "<args.report-writer-model>" \
179
179
  --related-tasks "<args.related-tasks>" \
180
180
  --clarification-response "<args.clarification-response>" \
181
- --pr-template-path "<args.pr-template-path>"
181
+ --pr-template-path "<args.pr-template-path>" \
182
+ --fix-cycle "<args.fix-cycle>"
182
183
  ```
183
184
 
184
185
  `render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full rendered lead prompt. Parse the labelled lines for `TASK_ROOT` and `INSTRUCTION_SET_PATH`. Also watch for an optional `okstra concurrent-run stages:` label line — present only when a concurrent run is detected (see "동시-run 감지 분기" below).
@@ -37,6 +37,7 @@ taskType: "{{FM_TASK_TYPE}}"
37
37
  - Next Recommended Phase: `{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}`
38
38
  - Awaiting Approval: `{{WORKFLOW_AWAITING_APPROVAL}}`
39
39
  - Routing Status: `{{WORKFLOW_ROUTING_STATUS}}`
40
+ - Fix cycles: {{FIX_CYCLES_SUMMARY}}
40
41
 
41
42
  ## Phase States
42
43
 
@@ -202,10 +202,10 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
202
202
 
203
203
  ### Validation Checklist{% if t("sectionAside.validationChecklist") != "Validation Checklist" %} ({{ t("sectionAside.validationChecklist") }}){% endif %}
204
204
 
205
- | Phase | Ticket ID | Check | Command / Observation | Expected outcome |
206
- |-------|-----------|-------|------------------------|-------------------|
205
+ | ID | Phase | Ticket ID | Check | Command / Observation | Expected outcome |
206
+ |----|-------|-----------|-------|------------------------|-------------------|
207
207
  {% for row in implementationPlanning.validationChecklist -%}
208
- | {{ row.phase }} | `{{ row.ticketId }}` | {{ row.check }} | `{{ row.commandOrObservation }}` | {{ row.expectedOutcome }} |
208
+ | {{ row.id }} | {{ row.phase }} | `{{ row.ticketId }}` | {{ row.check }} | `{{ row.commandOrObservation }}` | {{ row.expectedOutcome }} |
209
209
  {% endfor %}
210
210
 
211
211
  ### Rollback Strategy{% if t("sectionAside.rollbackStrategy") != "Rollback Strategy" %} ({{ t("sectionAside.rollbackStrategy") }}){% endif %}
@@ -556,6 +556,20 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
556
556
  | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
557
557
  |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|
558
558
 
559
+ {% endif %}
560
+ {% if fixCycle and fixCycle.cycle %}
561
+ ## 5.10 Fix History
562
+
563
+ > This run belongs to a post-release bug-fix cycle registered in `history/fix-cycles.jsonl`.
564
+
565
+ - Cycle: `{{ fixCycle.cycle }}` — {{ fixCycle.symptom }}
566
+ - Target report: `{{ fixCycle.targetReport }}`
567
+ {% if fixCycle.runs %}
568
+ - Runs in this cycle so far:
569
+ {% for r in fixCycle.runs %}
570
+ - {{ r.taskType }} seq {{ r.runSeq }}{% if r.runManifest %} (`{{ r.runManifest }}`){% endif %}
571
+ {% endfor %}
572
+ {% endif %}
559
573
  {% endif %}
560
574
  ## 6. Cross Verification Results
561
575
 
@@ -9,8 +9,11 @@
9
9
  * (everything else / fallback).
10
10
  * 2. Serialise the entries into markdown whose bytes are IDENTICAL
11
11
  * to scripts/okstra_ctl/report_views.py serialize_user_response.
12
- * 3. Write the result to <pre id="user-response-output"> and offer a
13
- * [Copy] button.
12
+ * 3. Write the result to <pre id="user-response-output">, offer a
13
+ * [Copy] button, and download it as
14
+ * user-response-<task-type>-<seq>.md — the user drops that file in
15
+ * runs/<task-type>/user-responses/ and --resume-clarification
16
+ * auto-appends it to the next run's clarification-response.md.
14
17
  *
15
18
  * The byte-identity contract is enforced by tests/test_report_views.py
16
19
  * which spawns Node to execute buildUserResponseMarkdown and diffs the
@@ -143,12 +146,30 @@
143
146
  return head + chunks;
144
147
  }
145
148
 
149
+ function downloadUserResponse(md, runMeta) {
150
+ var name =
151
+ "user-response-" + (runMeta["task-type"] || "report") + "-" +
152
+ (runMeta["seq"] || "000") + ".md";
153
+ var blob = new Blob([md], { type: "text/markdown" });
154
+ var url = URL.createObjectURL(blob);
155
+ var a = document.createElement("a");
156
+ a.href = url;
157
+ a.download = name;
158
+ document.body.appendChild(a);
159
+ a.click();
160
+ document.body.removeChild(a);
161
+ setTimeout(function () { URL.revokeObjectURL(url); }, 0);
162
+ }
163
+
146
164
  function exportUserResponse() {
147
165
  var runMeta = readRunMeta();
148
166
  var entries = collectEntries();
149
167
  var md = buildUserResponseMarkdown(runMeta, entries, isoNowUtc());
150
168
  var out = document.getElementById("user-response-output");
151
169
  if (out) out.textContent = md;
170
+ // Nothing answered yet — show the empty serialisation as feedback but
171
+ // don't download a useless file.
172
+ if (entries.length > 0) downloadUserResponse(md, runMeta);
152
173
  return md;
153
174
  }
154
175
 
@@ -1369,6 +1369,19 @@ def _data_path_for(report_path: Path) -> Path:
1369
1369
  return report_path.with_suffix(".data.json")
1370
1370
 
1371
1371
 
1372
+ def _load_final_report_data(report_path: Path) -> dict:
1373
+ """Best-effort parse of the final-report data.json sibling. Returns {} when
1374
+ absent or unparseable — those conditions are already surfaced as failures by
1375
+ validate_final_report_data; this loader only feeds cross-field checks."""
1376
+ data_path = _data_path_for(report_path)
1377
+ if not data_path.is_file():
1378
+ return {}
1379
+ try:
1380
+ return json.loads(data_path.read_text(encoding="utf-8"))
1381
+ except (OSError, json.JSONDecodeError):
1382
+ return {}
1383
+
1384
+
1372
1385
  def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
1373
1386
  """Validate the final-report data.json against the v1.0 schema.
1374
1387
 
@@ -1748,6 +1761,31 @@ def _validate_improvement_discovery(
1748
1761
  failures.append(f"improvement-discovery: {err}")
1749
1762
 
1750
1763
 
1764
+ def _validate_fix_cycle(run_manifest: dict, data: dict, failures: list[str]) -> None:
1765
+ """Enforce: when the run-manifest carries a fixCycleId, the final-report
1766
+ data.json MUST contain a fixCycle block whose ``cycle`` matches it.
1767
+
1768
+ Direction is one-way: a fixCycle block present without a run-manifest
1769
+ fixCycleId is NOT rejected — same posture as the schema-optional block.
1770
+ This check owns only the run→report direction (no missing/mismatched
1771
+ block when the run is attached), never the reverse."""
1772
+ cycle_id = (run_manifest or {}).get("fixCycleId", "")
1773
+ if not cycle_id:
1774
+ return
1775
+ block = (data or {}).get("fixCycle")
1776
+ if not isinstance(block, dict):
1777
+ failures.append(
1778
+ f"fix-cycle: run-manifest fixCycleId={cycle_id} but data.json has "
1779
+ "no fixCycle block"
1780
+ )
1781
+ return
1782
+ if block.get("cycle") != cycle_id:
1783
+ failures.append(
1784
+ f"fix-cycle: data.json fixCycle.cycle={block.get('cycle')!r} does "
1785
+ f"not match run-manifest fixCycleId={cycle_id!r}"
1786
+ )
1787
+
1788
+
1751
1789
  def _validate_session_conformance(
1752
1790
  team_state: dict,
1753
1791
  team_state_path: Path,
@@ -2157,6 +2195,9 @@ def main() -> int:
2157
2195
  # safety net for hand-edited or pre-v1.0 reports.
2158
2196
  task_type = effective_run_task_type(run_manifest, task_manifest)
2159
2197
  validate_final_report_data(report_path, failures)
2198
+ _validate_fix_cycle(
2199
+ run_manifest, _load_final_report_data(report_path), failures
2200
+ )
2160
2201
  validate_report(report_path, contract["required_agent_status_entries"], failures)
2161
2202
  validate_team_state_usage(team_state, failures)
2162
2203
 
@@ -18,11 +18,14 @@ Usage:
18
18
  [--gemini-model <m>] [--report-writer-model <m>] \\
19
19
  [--related-tasks <list>] [--base-ref <ref>] \\
20
20
  [--clarification-response <path>] [--work-category <cat>] \\
21
- [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>]
21
+ [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
22
+ [--fix-cycle <yes|no>]
22
23
 
23
24
  --stage implementation / final-verification only
24
25
  --stages release-handoff only: PR stage bundle csv (empty = whole-task)
25
26
  --pr-template-path release-handoff only
27
+ --fix-cycle entry phase only: record this re-entry of a done task as a
28
+ bug-fix cycle (yes opens/continues the cycle)
26
29
 
27
30
  release-handoff takes NO --task-brief (briefs belong to entry phases) —
28
31
  prepare generates the run's input document from the cited verification