okstra 0.172.0 → 0.174.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 (123) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture/storage-model.md +24 -3
  3. package/docs/architecture.md +21 -35
  4. package/docs/cli.md +39 -7
  5. package/docs/container.md +1 -1
  6. package/docs/contributor-change-matrix.md +1 -1
  7. package/docs/performance-improvement-plan-v2.md +6 -5
  8. package/docs/project-structure-overview.md +33 -25
  9. package/docs/task-process/README.md +6 -4
  10. package/docs/task-process/error-analysis.md +2 -2
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation-option-selection.md +70 -0
  13. package/docs/task-process/implementation-planning.md +24 -16
  14. package/docs/task-process/requirements-discovery.md +2 -2
  15. package/package.json +1 -1
  16. package/runtime/BUILD.json +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/report-writer-worker.md +30 -6
  19. package/runtime/bin/lib/okstra/cli.sh +5 -1
  20. package/runtime/bin/lib/okstra/globals.sh +2 -1
  21. package/runtime/bin/lib/okstra/usage.sh +3 -0
  22. package/runtime/bin/okstra-provider-exec.py +29 -12
  23. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  24. package/runtime/bin/okstra.sh +2 -0
  25. package/runtime/prompts/duties/direction-selection-worker.md +44 -0
  26. package/runtime/prompts/duties/planning-worker.md +12 -4
  27. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  28. package/runtime/prompts/lead/context-loader.md +1 -1
  29. package/runtime/prompts/lead/convergence.md +5 -5
  30. package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
  31. package/runtime/prompts/lead/plan-body-verification.md +23 -6
  32. package/runtime/prompts/lead/report-writer.md +33 -11
  33. package/runtime/prompts/profiles/_common-contract.md +3 -3
  34. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  35. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  36. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  37. package/runtime/prompts/profiles/error-analysis.md +4 -4
  38. package/runtime/prompts/profiles/final-verification.md +3 -3
  39. package/runtime/prompts/profiles/forbidden-actions.json +7 -0
  40. package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
  41. package/runtime/prompts/profiles/implementation-planning.md +61 -46
  42. package/runtime/prompts/profiles/implementation.md +4 -2
  43. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  44. package/runtime/prompts/profiles/release-handoff.md +1 -1
  45. package/runtime/prompts/profiles/requirements-discovery.md +3 -3
  46. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  47. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  48. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  49. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
  50. package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
  51. package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
  52. package/runtime/python/okstra_ctl/conformance.py +68 -0
  53. package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
  54. package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
  55. package/runtime/python/okstra_ctl/doctor.py +2 -2
  56. package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
  57. package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
  58. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  59. package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
  60. package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
  61. package/runtime/python/okstra_ctl/implementation_options.py +479 -0
  62. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  63. package/runtime/python/okstra_ctl/plan_items.py +51 -3
  64. package/runtime/python/okstra_ctl/render.py +1 -0
  65. package/runtime/python/okstra_ctl/render_final_report.py +16 -19
  66. package/runtime/python/okstra_ctl/report_contract.py +45 -14
  67. package/runtime/python/okstra_ctl/report_finalize.py +68 -9
  68. package/runtime/python/okstra_ctl/report_html/render.py +4 -2
  69. package/runtime/python/okstra_ctl/report_html/router.py +4 -0
  70. package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
  71. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
  72. package/runtime/python/okstra_ctl/report_views.py +148 -12
  73. package/runtime/python/okstra_ctl/run.py +393 -4
  74. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  75. package/runtime/python/okstra_ctl/scope_provenance.py +16 -10
  76. package/runtime/python/okstra_ctl/session.py +69 -12
  77. package/runtime/python/okstra_ctl/team.py +51 -25
  78. package/runtime/python/okstra_ctl/tmux.py +19 -149
  79. package/runtime/python/okstra_ctl/user_response.py +75 -0
  80. package/runtime/python/okstra_ctl/wizard.py +144 -0
  81. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  82. package/runtime/python/okstra_ctl/worker_request.py +2 -0
  83. package/runtime/python/okstra_ctl/workflow.py +29 -7
  84. package/runtime/python/okstra_ctl/worktree.py +69 -3
  85. package/runtime/python/okstra_token_usage/cli.py +1 -1
  86. package/runtime/python/okstra_token_usage/collect.py +66 -6
  87. package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
  88. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  89. package/runtime/templates/reports/final-report-v2.template.md +4 -0
  90. package/runtime/templates/reports/final-verification-input.template.md +1 -1
  91. package/runtime/templates/reports/html/base.template.html +3 -2
  92. package/runtime/templates/reports/html/i18n/en.json +21 -1
  93. package/runtime/templates/reports/html/i18n/ko.json +21 -1
  94. package/runtime/templates/reports/html/macros/forms.html +21 -2
  95. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
  96. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
  97. package/runtime/templates/reports/i18n/en.json +13 -0
  98. package/runtime/templates/reports/implementation-input.template.md +4 -2
  99. package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
  100. package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
  101. package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
  102. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
  103. package/runtime/templates/reports/report.js +111 -4
  104. package/runtime/templates/reports/settings.template.json +0 -24
  105. package/runtime/templates/reports/task-brief.template.md +9 -3
  106. package/runtime/templates/reports/user-response.template.md +25 -4
  107. package/runtime/templates/worker-prompt-preamble.md +8 -0
  108. package/runtime/validators/lib/fixtures.sh +49 -17
  109. package/runtime/validators/validate-implementation-plan-stages.py +169 -4
  110. package/runtime/validators/validate-report-views.py +2 -2
  111. package/runtime/validators/validate-run.py +149 -498
  112. package/runtime/validators/validate_improvement_report.py +5 -1
  113. package/runtime/validators/validate_session_conformance.py +1 -1
  114. package/src/cli-registry.mjs +8 -1
  115. package/src/commands/execute/codex-run.mjs +1 -0
  116. package/src/commands/execute/render-bundle.mjs +1 -0
  117. package/src/commands/execute/team.mjs +3 -3
  118. package/src/commands/execute/worktree-status.mjs +109 -0
  119. package/src/commands/lifecycle/install.mjs +0 -2
  120. package/src/commands/report/finalize.mjs +13 -6
  121. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  122. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  123. package/runtime/templates/reports/final-report.template.md +0 -1258
@@ -12,10 +12,10 @@ columns in the Execution Status table, omitted §4 phase-continuation
12
12
  rows, ad-hoc ``## Index`` sections. Routing everything through one
13
13
  template + schema cuts those failure modes to zero.
14
14
 
15
- For schema-v1 compatibility, the top-of-report ``## Index`` remains a
16
- deterministic post-render section built by ``_inject_index_and_anchors``.
17
- Schema v2 keeps its compact fixed AI-handoff order and does not inject the
18
- legacy reader index; the task-specific HTML provides the human navigation.
15
+ Rendering never injects a reader index: the AI-handoff markdown keeps its
16
+ compact fixed order and the task-specific HTML provides human navigation.
17
+ ``_inject_index_and_anchors`` survives as a standalone pass over an already
18
+ written markdown file, driven by ``scripts/okstra-inject-report-index.py``.
19
19
 
20
20
  Phase 7 mutation flow: ``okstra-token-usage.py --substitute-data`` fills
21
21
  the ``tokenUsage`` and ``executionStatus[].totalTokens`` etc. cells in
@@ -53,7 +53,11 @@ from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_ji
53
53
  from okstra_ctl.md_table import UNESCAPED_PIPE_RE, to_cell_text
54
54
  from okstra_ctl.models import UnknownModelError, resolve_model_metadata
55
55
  from okstra_ctl.paths import find_asset_root
56
- from okstra_ctl.report_contract import TASK_TYPE_DATA_PROPERTY, markdown_template_for
56
+ from okstra_ctl.report_contract import (
57
+ CURRENT_REPORT_SCHEMA_VERSION,
58
+ TASK_TYPE_DATA_PROPERTY,
59
+ markdown_template_for,
60
+ )
57
61
  from okstra_ctl.report_markdown import ReportSections
58
62
  from okstra_ctl.schema_excerpt import excerpt_cut_from_version
59
63
  from okstra_ctl.seeding import installed_version
@@ -61,15 +65,15 @@ from okstra_ctl.usage_cells import format_duration_ms, format_int, format_usd
61
65
 
62
66
 
63
67
  TEMPLATE_BY_SCHEMA_VERSION = {
64
- "1.0": ("templates", "reports", "final-report.template.md"),
65
68
  "2.0": ("templates", "reports", "final-report-v2.template.md"),
66
69
  }
67
- DEFAULT_TEMPLATE_REL = TEMPLATE_BY_SCHEMA_VERSION["1.0"]
70
+ DEFAULT_TEMPLATE_REL = TEMPLATE_BY_SCHEMA_VERSION[CURRENT_REPORT_SCHEMA_VERSION]
68
71
 
69
72
  TASK_DELIVERABLE_TITLES = {
70
73
  "requirements-discovery": "Requirements Discovery",
71
74
  "improvement-discovery": "Improvement Discovery",
72
75
  "error-analysis": "Error Analysis",
76
+ "implementation-option-selection": "Implementation Option Selection",
73
77
  "project-analysis": "Project Analysis",
74
78
  "feature-analysis": "Feature Analysis",
75
79
  "change-impact-analysis": "Change Impact Analysis",
@@ -665,14 +669,7 @@ def render(
665
669
 
666
670
  try:
667
671
  template = env.get_template(template_path.name)
668
- context = (
669
- _ai_markdown_context(data, schema)
670
- if data.get("schemaVersion") == "2.0"
671
- else _with_optional_defaults(data)
672
- )
673
- rendered = template.render(**context)
674
- if data.get("schemaVersion") == "1.0":
675
- rendered = _inject_index_and_anchors(rendered, dictionary)
672
+ rendered = template.render(**_ai_markdown_context(data, schema))
676
673
  return _ventilate_prose(rendered)
677
674
  except I18nError as exc:
678
675
  raise FinalReportRenderError(
@@ -688,10 +685,10 @@ def find_default_template(start: Path | None = None) -> Path:
688
685
  """Locate the bundled final-report template.
689
686
 
690
687
  Resolution order:
691
- 1. ``$OKSTRA_HOME/templates/reports/final-report.template.md`` (installed runtime).
692
- 2. ``<repo>/templates/reports/final-report.template.md`` (in-repo dev runs).
688
+ 1. ``$OKSTRA_HOME/templates/reports/final-report-v2.template.md`` (installed runtime).
689
+ 2. ``<repo>/templates/reports/final-report-v2.template.md`` (in-repo dev runs).
693
690
  Repo root is detected by walking up from this file until a
694
- ``templates/reports/final-report.template.md`` exists.
691
+ ``templates/reports/final-report-v2.template.md`` exists.
695
692
 
696
693
  Raises ``FinalReportRenderError`` if neither path is present.
697
694
  """
@@ -700,7 +697,7 @@ def find_default_template(start: Path | None = None) -> Path:
700
697
  return root.joinpath(*DEFAULT_TEMPLATE_REL)
701
698
 
702
699
  raise FinalReportRenderError(
703
- "could not locate final-report.template.md. Set OKSTRA_HOME or "
700
+ "could not locate final-report-v2.template.md. Set OKSTRA_HOME or "
704
701
  "run from a checkout that contains templates/reports/."
705
702
  )
706
703
 
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
 
4
4
 
5
5
  CURRENT_REPORT_SCHEMA_VERSION = "2.0"
6
- LEGACY_REPORT_SCHEMA_VERSION = "1.0"
7
6
 
8
7
  PUBLIC_REPORT_TASK_TYPES = (
9
8
  "requirements-discovery",
10
9
  "improvement-discovery",
11
10
  "error-analysis",
11
+ "implementation-option-selection",
12
12
  "project-analysis",
13
13
  "feature-analysis",
14
14
  "change-impact-analysis",
@@ -22,6 +22,7 @@ TASK_TYPE_DATA_PROPERTY = {
22
22
  "requirements-discovery": "requirementsDiscovery",
23
23
  "improvement-discovery": "improvementDiscovery",
24
24
  "error-analysis": "errorAnalysis",
25
+ "implementation-option-selection": "implementationOptionSelection",
25
26
  "project-analysis": "projectAnalysis",
26
27
  "feature-analysis": "featureAnalysis",
27
28
  "change-impact-analysis": "changeImpactAnalysis",
@@ -63,6 +64,12 @@ TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
63
64
  "errorAnalysis.causeCandidates.falsifyingEvidenceChecked",
64
65
  "errorAnalysis.causeCandidates.disproveWith",
65
66
  ),
67
+ "implementation-option-selection": (
68
+ "implementationOptionSelection.decisionContext.originalRequirementIds",
69
+ "implementationOptionSelection.rankedOptions.requirementCoverage",
70
+ "implementationOptionSelection.rankedOptions.coverageSummary",
71
+ "implementationOptionSelection.recommendedOptionId",
72
+ ),
66
73
  "project-analysis": (
67
74
  "projectAnalysis.components",
68
75
  "projectAnalysis.entryPoints",
@@ -89,20 +96,12 @@ TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
89
96
  "changeImpactAnalysis.securityAndPerformanceImpact",
90
97
  "changeImpactAnalysis.planningInputs",
91
98
  ),
92
- # `validationChecklist` / `rollbackStrategy` are deliberately NOT here. The
93
- # planning HTML is the approver's document, and both tables are the
94
- # implementer's and auditor's working material — the reasoning is written
95
- # into `html/tasks/implementation-planning.template.html` above the
96
- # requirement-coverage section, and both still render in the markdown report
97
- # (`final-report.template.md` §"Validation Checklist" / §"Rollback
98
- # Strategy"). When that split landed the template dropped the sections and
99
- # this list kept demanding them, so `validate-report-views.py` failed every
100
- # implementation-planning report with nothing a report could do about it.
99
+ # Legacy plans render option comparison fields; selected-direction plans
100
+ # render the selected reference, its realization, and exact coverage. The
101
+ # task-level marker is the only field common to both HTML branches. Each
102
+ # branch's required details are covered by its schema and renderer tests.
101
103
  "implementation-planning": (
102
- "implementationPlanning.optionCandidates",
103
- "implementationPlanning.tradeoffMatrix",
104
- "implementationPlanning.recommendedOption",
105
- "implementationPlanning.stageMap",
104
+ "implementationPlanning",
106
105
  ),
107
106
  "implementation": (
108
107
  "implementation.diffSummary",
@@ -124,6 +123,38 @@ TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
124
123
  ),
125
124
  }
126
125
 
126
+ IMPLEMENTATION_PLANNING_LEGACY_REQUIRED_HUMAN_FIELDS = (
127
+ "implementationPlanning.optionCandidates",
128
+ "implementationPlanning.tradeoffMatrix",
129
+ "implementationPlanning.recommendedOption",
130
+ "implementationPlanning.stageMap",
131
+ )
132
+
133
+ IMPLEMENTATION_PLANNING_PLAN_READY_REQUIRED_HUMAN_FIELDS = (
134
+ "implementationPlanning.selectedDirectionRef",
135
+ "implementationPlanning.directionRealization",
136
+ "implementationPlanning.coverageSummary",
137
+ "implementationPlanning.stageMap",
138
+ "implementationPlanning.requirementCoverage",
139
+ )
140
+
141
+ IMPLEMENTATION_PLANNING_INVALIDATED_REQUIRED_HUMAN_FIELDS = (
142
+ "implementationPlanning.selectedDirectionRef",
143
+ "implementationPlanning.directionInvalidation",
144
+ "implementationPlanning.routing",
145
+ )
146
+
147
+
148
+ def required_human_fields_for_report(task_type: str, data: dict) -> tuple[str, ...]:
149
+ if task_type != "implementation-planning":
150
+ return TASK_TYPE_REQUIRED_HUMAN_FIELDS.get(task_type, ())
151
+ planning = data.get("implementationPlanning", {})
152
+ if planning.get("planningContract") != "selected-direction":
153
+ return IMPLEMENTATION_PLANNING_LEGACY_REQUIRED_HUMAN_FIELDS
154
+ if planning.get("outcome") == "direction-invalidated":
155
+ return IMPLEMENTATION_PLANNING_INVALIDATED_REQUIRED_HUMAN_FIELDS
156
+ return IMPLEMENTATION_PLANNING_PLAN_READY_REQUIRED_HUMAN_FIELDS
157
+
127
158
 
128
159
  def report_property_for(task_type: str) -> str:
129
160
  try:
@@ -6,6 +6,18 @@ substitution, html view rendering, follow-up task spawning, and run validation.
6
6
  The order is load-bearing — rendering before substitution ships `--` token
7
7
  cells, and validating before rendering trips the report-views contract.
8
8
 
9
+ `token-usage` is the one step whose failure does not stop the sequence, which
10
+ deliberately accepts that first state: its input is the lead session log, so a
11
+ refusal there would otherwise delete every artifact the later steps produce.
12
+ The run does not pass in that state. `validators/validate-run.py` re-collects
13
+ when the recorded usage is all zeros against an `unavailable` session source
14
+ (`_needs_token_autofix`) and refuses the run rather than ship zeroed counts
15
+ (`accuracy-failed`); a legacy v1 report is caught earlier still, by its
16
+ unsubstituted `{{...}}` placeholders, which a v2 report never carries because
17
+ its numeric cells are `null` until this step fills them. Substituting the
18
+ tokens on a later retry then leaves the already-rendered html stale for
19
+ `validators/validate-report-views.py`.
20
+
9
21
  The translation sidecar is NOT one of these steps. `render-views` overlays it,
10
22
  so a non-English run dispatches the translator before this sequence starts —
11
23
  after verifying the data.json is English, which is why `check-source` is also
@@ -29,6 +41,7 @@ from .agent_activity import ActivityProjectionError, project_agent_activity
29
41
  from .dispatch_state import DispatchError, link_agent_dispatch_result
30
42
  from .final_report_paths import final_report_data_path, final_report_markdown_path
31
43
  from .paths import task_dir, task_manifest_file
44
+ from .session import observe_lead_session
32
45
 
33
46
 
34
47
  STEP_PROJECT_ACTIVITY = "project-activity"
@@ -354,7 +367,14 @@ def run_finalize(
354
367
  before_step: Callable[[str], None] | None = None,
355
368
  only: Sequence[str] | None = None,
356
369
  ) -> dict[str, Any]:
357
- """Run the Phase 7 steps in order, stopping at the first non-zero exit.
370
+ """Run the Phase 7 steps in contractual order.
371
+
372
+ A non-zero exit stops the sequence, with one exception: ``token-usage``
373
+ defers. Its input is the lead session log — state outside this run — so a
374
+ refusal there is not evidence that the artifacts after it are unwritable.
375
+ The deferred failure becomes the result's ``reason`` when nothing later
376
+ fails, and yields to any later failure, which is the step that actually
377
+ blocked the run.
358
378
 
359
379
  ``before_step`` fires immediately before each step is spawned, letting an
360
380
  adapter settle state the step will read (the Codex adapter marks the report
@@ -366,6 +386,7 @@ def run_finalize(
366
386
  step at full token and wall-clock cost.
367
387
  """
368
388
  steps: list[dict[str, Any]] = []
389
+ deferred = ""
369
390
  try:
370
391
  commands = build_commands(ctx)
371
392
  except FinalizeError as exc:
@@ -426,11 +447,21 @@ def run_finalize(
426
447
  )
427
448
  steps.append(step_payload(name, command, result))
428
449
  if result.returncode != 0:
429
- return {
430
- "ok": False,
431
- "reason": f"{name} failed with exit code {result.returncode}",
432
- "steps": steps,
433
- }
450
+ failure = f"{name} failed with exit code {result.returncode}"
451
+ # Token collection reads the lead session log, which lives outside
452
+ # this run. One refusal there (`grandTotalTokens=0`) took the whole
453
+ # run's output with it: html never rendered, follow-ups never
454
+ # spawned, and `validate-run` then blocked on `report-views:
455
+ # missing html artifact`. So this step alone defers instead of
456
+ # stopping the sequence. It hides nothing — the non-zero exit stays
457
+ # in `steps`, `ok` stays False, and the closing `validate-run`
458
+ # still refuses the run.
459
+ if name == STEP_TOKEN_USAGE:
460
+ deferred = failure
461
+ continue
462
+ return {"ok": False, "reason": failure, "steps": steps}
463
+ if deferred:
464
+ return {"ok": False, "reason": deferred, "steps": steps}
434
465
  return {"ok": True, "reason": "", "steps": steps}
435
466
 
436
467
 
@@ -546,6 +577,29 @@ def _step_summary_lines(result: Mapping[str, Any]) -> list[str]:
546
577
  return lines
547
578
 
548
579
 
580
+ def _recovery_step_names(result: Mapping[str, Any]) -> list[str]:
581
+ """The steps a retry has to re-run: every step from the earliest failure on.
582
+
583
+ Naming only the failed steps would prescribe half a recovery. `token-usage`
584
+ defers, so `render-views` already wrote an html view from unsubstituted
585
+ data; substituting the tokens on a retry leaves that view stale
586
+ (`validators/validate-report-views.py` checks `source-sha256` against the
587
+ md body). And a sequence that stopped early never reached `validate-run`,
588
+ which is the step that decides whether the run is shippable. Resuming from
589
+ the earliest failure redoes both while still skipping the prefix that
590
+ succeeded — the saving `--only` exists for.
591
+ """
592
+ failed = {
593
+ string_value(step.get("name"))
594
+ for step in (result.get("steps") or [])
595
+ if step.get("exitCode") != 0
596
+ }
597
+ for index, name in enumerate(STEP_ORDER):
598
+ if name in failed:
599
+ return list(STEP_ORDER[index:])
600
+ return []
601
+
602
+
549
603
  def main(argv: Sequence[str] | None = None) -> int:
550
604
  args = _parser().parse_args(argv)
551
605
  try:
@@ -553,6 +607,10 @@ def main(argv: Sequence[str] | None = None) -> int:
553
607
  except FinalizeError as exc:
554
608
  print(f"error: {exc}", file=sys.stderr)
555
609
  return 2
610
+ # Phase 7 is the last boundary the lead crosses, and the last chance to add
611
+ # the generations resume and compaction split it into. It runs ahead of the
612
+ # sequence because the token-usage step below reads `leadSessionIds`.
613
+ observe_lead_session(ctx.project_root, ctx.team_state_path)
556
614
  result = run_finalize(ctx, only=args.only or None)
557
615
  print(json.dumps(result, indent=2, ensure_ascii=False))
558
616
  print("finalize steps:", file=sys.stderr)
@@ -560,10 +618,11 @@ def main(argv: Sequence[str] | None = None) -> int:
560
618
  print(line, file=sys.stderr)
561
619
  if not result["ok"]:
562
620
  print(f"error: {result['reason']}", file=sys.stderr)
563
- if result.get("steps"):
621
+ recovery = _recovery_step_names(result)
622
+ if recovery:
623
+ flags = " ".join(f"--only {name}" for name in recovery)
564
624
  print(
565
- "retry just the failing step with "
566
- f"`--only {result['steps'][-1].get('name')}`",
625
+ f"resume the sequence from the earliest failure with `{flags}`",
567
626
  file=sys.stderr,
568
627
  )
569
628
  return 1
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  import okstra_vendor # noqa: F401 # registers vendored dependency aliases
10
10
  from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
11
11
 
12
- from ..final_report_paths import translation_sidecar_path
12
+ from ..final_report_paths import final_report_data_path, translation_sidecar_path
13
13
  from ..final_report_schema import load_schema_for_data, validate
14
14
  from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
15
15
  from ..report_translation import overlay
@@ -142,11 +142,13 @@ def render_v2_html_view(
142
142
  env.filters["paragraphs"] = lambda value: paragraphs(value, anchors)
143
143
  response_js = (root / "report.js").read_text(encoding="utf-8")
144
144
  base_js = (root / "html/assets/base.js").read_text(encoding="utf-8")
145
+ source_data = final_report_data_path(Path(run_meta.source_report)).as_posix()
145
146
  context = {
146
147
  **view.context,
147
148
  "runMeta": run_meta,
148
149
  "reportMeta": _report_meta(data, run_meta),
149
150
  "taskType": view.task_type,
151
+ "sourceData": source_data,
150
152
  "dataSha256": _sha256(data_path),
151
153
  "markdownSha256": _sha256(markdown_path),
152
154
  "clarificationItems": data.get("clarificationItems", []),
@@ -161,7 +163,7 @@ def render_v2_html_view(
161
163
  output_path.write_text(
162
164
  inject_report_index(document, label=translate("base.contents")), encoding="utf-8"
163
165
  )
164
- if context["clarificationItems"]:
166
+ if context["clarificationItems"] or context.get("directionSelection"):
165
167
  # The footer tells the reader to drop the exported file here, so the
166
168
  # directory has to exist before they go looking for it.
167
169
  user_responses_dir_for_report(data_path).mkdir(parents=True, exist_ok=True)
@@ -8,6 +8,9 @@ from .view_models.error_analysis import build_error_analysis_view
8
8
  from .view_models.feature_analysis import build_feature_analysis_view
9
9
  from .view_models.final_verification import build_final_verification_view
10
10
  from .view_models.improvement_discovery import build_improvement_discovery_view
11
+ from .view_models.implementation_option_selection import (
12
+ build_implementation_option_selection_view,
13
+ )
11
14
  from .view_models.implementation import build_implementation_view
12
15
  from .view_models.implementation_planning import build_implementation_planning_view
13
16
  from .view_models.project_analysis import build_project_analysis_view
@@ -23,6 +26,7 @@ VIEW_BUILDERS = {
23
26
  "requirements-discovery": build_requirements_discovery_view,
24
27
  "improvement-discovery": build_improvement_discovery_view,
25
28
  "error-analysis": build_error_analysis_view,
29
+ "implementation-option-selection": build_implementation_option_selection_view,
26
30
  "project-analysis": build_project_analysis_view,
27
31
  "feature-analysis": build_feature_analysis_view,
28
32
  "change-impact-analysis": build_change_impact_analysis_view,
@@ -0,0 +1,32 @@
1
+ """Human-first implementation-direction comparison view."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import evidence_index
5
+ from ..models import HumanReportView
6
+
7
+
8
+ def build_implementation_option_selection_view(data: dict) -> HumanReportView:
9
+ selection = data["implementationOptionSelection"]
10
+ context = {
11
+ "humanSummary": data["humanSummary"],
12
+ "selection": selection,
13
+ "directionSelection": (
14
+ selection if selection["mode"] == "candidate-comparison" else None
15
+ ),
16
+ "narrative": selection["userNarrative"],
17
+ "recommendedOption": next(
18
+ (
19
+ option
20
+ for option in selection["rankedOptions"]
21
+ if option["id"] == selection["recommendedOptionId"]
22
+ ),
23
+ None,
24
+ ),
25
+ "evidenceIndex": evidence_index(data),
26
+ }
27
+ return HumanReportView(
28
+ task_type="implementation-option-selection",
29
+ template_name="html/tasks/implementation-option-selection.template.html",
30
+ context=context,
31
+ figures=(),
32
+ )
@@ -15,6 +15,8 @@ class PlanApprovalState:
15
15
  recommended_option: str
16
16
  disabled_reason: str
17
17
  blocker_ids: tuple[str, ...]
18
+ show_option_selector: bool = True
19
+ reentry_command: str = ""
18
20
 
19
21
 
20
22
  def resolve_recommended_option(rec_name: str, names: tuple[str, ...]) -> str:
@@ -32,6 +34,26 @@ def plan_approval_state(data: dict) -> PlanApprovalState | None:
32
34
  planning = data.get("implementationPlanning")
33
35
  if not isinstance(planning, dict):
34
36
  return None
37
+ blockers = tuple(
38
+ row["id"]
39
+ for row in data.get("clarificationItems", [])
40
+ if row.get("blocks") == "approval"
41
+ and row.get("status") in {"open", "answered"}
42
+ )
43
+ reason = f"{len(blockers)} approval blocker(s) unresolved" if blockers else ""
44
+ if planning.get("planningContract") == "selected-direction":
45
+ if planning.get("outcome") == "direction-invalidated" and not reason:
46
+ reason = "selected direction invalidated"
47
+ task_key = (data.get("header") or {}).get("taskKey", "")
48
+ reentry_command = (
49
+ f"/okstra-run task-key={task_key} "
50
+ "task-type=implementation-option-selection"
51
+ if planning.get("outcome") == "direction-invalidated" and task_key
52
+ else ""
53
+ )
54
+ return PlanApprovalState(
55
+ (), "", reason, blockers, False, reentry_command
56
+ )
35
57
  names = tuple(
36
58
  row["name"]
37
59
  for row in planning.get("optionCandidates", [])
@@ -40,13 +62,6 @@ def plan_approval_state(data: dict) -> PlanApprovalState | None:
40
62
  if not names:
41
63
  return None
42
64
  recommended = planning.get("recommendedOption", {}).get("name", "")
43
- blockers = tuple(
44
- row["id"]
45
- for row in data.get("clarificationItems", [])
46
- if row.get("blocks") == "approval"
47
- and row.get("status") in {"open", "answered"}
48
- )
49
- reason = f"{len(blockers)} approval blocker(s) unresolved" if blockers else ""
50
65
  return PlanApprovalState(
51
66
  names,
52
67
  resolve_recommended_option(recommended, names),
@@ -89,7 +104,7 @@ _OMITTED_FIELDS = (
89
104
 
90
105
  def build_implementation_planning_view(data: dict) -> HumanReportView:
91
106
  planning = data["implementationPlanning"]
92
- figure = _stage_figure(planning)
107
+ figure = _stage_figure(planning) if planning.get("stageMap") else None
93
108
  approval = plan_approval_state(data)
94
109
  activities = tuple(
95
110
  row for row in data.get("agentActivity", []) if isinstance(row, dict)
@@ -104,7 +119,7 @@ def build_implementation_planning_view(data: dict) -> HumanReportView:
104
119
  context = {
105
120
  "humanSummary": data["humanSummary"],
106
121
  "planning": planning,
107
- "narrative": planning["userNarrative"],
122
+ "narrative": planning.get("userNarrative", {}),
108
123
  "stageFigure": figure,
109
124
  "approval": approval,
110
125
  "openDecisions": [
@@ -125,6 +140,6 @@ def build_implementation_planning_view(data: dict) -> HumanReportView:
125
140
  "implementation-planning",
126
141
  "html/tasks/implementation-planning.template.html",
127
142
  context,
128
- (figure,),
143
+ (figure,) if figure is not None else (),
129
144
  _OMITTED_FIELDS,
130
145
  )