okstra 0.171.0 → 0.173.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 (106) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture/storage-model.md +11 -0
  3. package/docs/architecture.md +29 -14
  4. package/docs/cli.md +40 -7
  5. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  6. package/docs/performance-improvement-plan-v2.md +6 -5
  7. package/docs/project-structure-overview.md +24 -14
  8. package/docs/task-process/README.md +5 -3
  9. package/docs/task-process/error-analysis.md +2 -2
  10. package/docs/task-process/final-verification.md +2 -2
  11. package/docs/task-process/implementation-option-selection.md +70 -0
  12. package/docs/task-process/implementation-planning.md +23 -15
  13. package/docs/task-process/requirements-discovery.md +2 -2
  14. package/package.json +1 -1
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/agents/workers/report-writer-worker.md +30 -6
  17. package/runtime/bin/lib/okstra/cli.sh +5 -1
  18. package/runtime/bin/lib/okstra/globals.sh +1 -0
  19. package/runtime/bin/lib/okstra/usage.sh +3 -0
  20. package/runtime/bin/okstra.sh +2 -0
  21. package/runtime/prompts/duties/direction-selection-worker.md +44 -0
  22. package/runtime/prompts/duties/planning-worker.md +12 -4
  23. package/runtime/prompts/launch.template.md +4 -0
  24. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  25. package/runtime/prompts/lead/context-loader.md +1 -1
  26. package/runtime/prompts/lead/convergence.md +5 -5
  27. package/runtime/prompts/lead/okstra-lead-contract.md +42 -17
  28. package/runtime/prompts/lead/plan-body-verification.md +42 -14
  29. package/runtime/prompts/lead/report-writer.md +38 -15
  30. package/runtime/prompts/lead/team-contract.md +2 -0
  31. package/runtime/prompts/profiles/_clarification-recommendation.md +3 -1
  32. package/runtime/prompts/profiles/_common-contract.md +3 -2
  33. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  34. package/runtime/prompts/profiles/error-analysis.md +3 -3
  35. package/runtime/prompts/profiles/final-verification.md +3 -3
  36. package/runtime/prompts/profiles/forbidden-actions.json +7 -0
  37. package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
  38. package/runtime/prompts/profiles/implementation-planning.md +56 -37
  39. package/runtime/prompts/profiles/implementation.md +2 -1
  40. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  41. package/runtime/prompts/profiles/requirements-discovery.md +3 -3
  42. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  43. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
  44. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
  45. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  46. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  47. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
  48. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
  49. package/runtime/python/okstra_ctl/agent_activity.py +306 -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/clarification_items.py +37 -20
  53. package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
  54. package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
  55. package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
  56. package/runtime/python/okstra_ctl/implementation_options.py +479 -0
  57. package/runtime/python/okstra_ctl/lead_events.py +47 -4
  58. package/runtime/python/okstra_ctl/plan_items.py +51 -3
  59. package/runtime/python/okstra_ctl/render.py +12 -3
  60. package/runtime/python/okstra_ctl/render_final_report.py +1 -0
  61. package/runtime/python/okstra_ctl/report_contract.py +45 -13
  62. package/runtime/python/okstra_ctl/report_finalize.py +51 -14
  63. package/runtime/python/okstra_ctl/report_html/common.py +5 -3
  64. package/runtime/python/okstra_ctl/report_html/render.py +4 -2
  65. package/runtime/python/okstra_ctl/report_html/router.py +4 -0
  66. package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
  67. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +42 -11
  68. package/runtime/python/okstra_ctl/report_translation.py +14 -0
  69. package/runtime/python/okstra_ctl/report_views.py +148 -12
  70. package/runtime/python/okstra_ctl/run.py +350 -2
  71. package/runtime/python/okstra_ctl/scope_provenance.py +15 -9
  72. package/runtime/python/okstra_ctl/user_response.py +75 -0
  73. package/runtime/python/okstra_ctl/wizard.py +144 -0
  74. package/runtime/python/okstra_ctl/worker_audit_ledger.py +150 -0
  75. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  76. package/runtime/python/okstra_ctl/workflow.py +29 -7
  77. package/runtime/schemas/final-report-v2.0.schema.json +1623 -143
  78. package/runtime/skills/okstra-user-response/SKILL.md +2 -2
  79. package/runtime/templates/reports/final-report-v2.template.md +12 -0
  80. package/runtime/templates/reports/final-verification-input.template.md +1 -1
  81. package/runtime/templates/reports/html/assets/base.css +7 -0
  82. package/runtime/templates/reports/html/base.template.html +3 -2
  83. package/runtime/templates/reports/html/i18n/en.json +27 -2
  84. package/runtime/templates/reports/html/i18n/ko.json +27 -2
  85. package/runtime/templates/reports/html/macros/forms.html +42 -4
  86. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
  87. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +61 -2
  88. package/runtime/templates/reports/i18n/en.json +17 -0
  89. package/runtime/templates/reports/implementation-input.template.md +4 -2
  90. package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
  91. package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
  92. package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
  93. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
  94. package/runtime/templates/reports/report.js +137 -21
  95. package/runtime/templates/reports/task-brief.template.md +9 -3
  96. package/runtime/templates/reports/user-response.template.md +28 -5
  97. package/runtime/templates/worker-prompt-preamble.md +16 -0
  98. package/runtime/validators/validate-implementation-plan-stages.py +106 -1
  99. package/runtime/validators/validate-report-views.py +2 -2
  100. package/runtime/validators/validate-run.py +1124 -54
  101. package/runtime/validators/validate_improvement_report.py +5 -1
  102. package/runtime/validators/validate_session_conformance.py +523 -35
  103. package/src/cli-registry.mjs +7 -0
  104. package/src/commands/execute/codex-run.mjs +1 -0
  105. package/src/commands/execute/render-bundle.mjs +1 -0
  106. package/src/commands/report/agent-activity.mjs +21 -0
@@ -9,6 +9,7 @@ PUBLIC_REPORT_TASK_TYPES = (
9
9
  "requirements-discovery",
10
10
  "improvement-discovery",
11
11
  "error-analysis",
12
+ "implementation-option-selection",
12
13
  "project-analysis",
13
14
  "feature-analysis",
14
15
  "change-impact-analysis",
@@ -22,6 +23,7 @@ TASK_TYPE_DATA_PROPERTY = {
22
23
  "requirements-discovery": "requirementsDiscovery",
23
24
  "improvement-discovery": "improvementDiscovery",
24
25
  "error-analysis": "errorAnalysis",
26
+ "implementation-option-selection": "implementationOptionSelection",
25
27
  "project-analysis": "projectAnalysis",
26
28
  "feature-analysis": "featureAnalysis",
27
29
  "change-impact-analysis": "changeImpactAnalysis",
@@ -63,6 +65,12 @@ TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
63
65
  "errorAnalysis.causeCandidates.falsifyingEvidenceChecked",
64
66
  "errorAnalysis.causeCandidates.disproveWith",
65
67
  ),
68
+ "implementation-option-selection": (
69
+ "implementationOptionSelection.decisionContext.originalRequirementIds",
70
+ "implementationOptionSelection.rankedOptions.requirementCoverage",
71
+ "implementationOptionSelection.rankedOptions.coverageSummary",
72
+ "implementationOptionSelection.recommendedOptionId",
73
+ ),
66
74
  "project-analysis": (
67
75
  "projectAnalysis.components",
68
76
  "projectAnalysis.entryPoints",
@@ -89,20 +97,12 @@ TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
89
97
  "changeImpactAnalysis.securityAndPerformanceImpact",
90
98
  "changeImpactAnalysis.planningInputs",
91
99
  ),
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.
100
+ # Legacy plans render option comparison fields; selected-direction plans
101
+ # render the selected reference, its realization, and exact coverage. The
102
+ # task-level marker is the only field common to both HTML branches. Each
103
+ # branch's required details are covered by its schema and renderer tests.
101
104
  "implementation-planning": (
102
- "implementationPlanning.optionCandidates",
103
- "implementationPlanning.tradeoffMatrix",
104
- "implementationPlanning.recommendedOption",
105
- "implementationPlanning.stageMap",
105
+ "implementationPlanning",
106
106
  ),
107
107
  "implementation": (
108
108
  "implementation.diffSummary",
@@ -124,6 +124,38 @@ TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
124
124
  ),
125
125
  }
126
126
 
127
+ IMPLEMENTATION_PLANNING_LEGACY_REQUIRED_HUMAN_FIELDS = (
128
+ "implementationPlanning.optionCandidates",
129
+ "implementationPlanning.tradeoffMatrix",
130
+ "implementationPlanning.recommendedOption",
131
+ "implementationPlanning.stageMap",
132
+ )
133
+
134
+ IMPLEMENTATION_PLANNING_PLAN_READY_REQUIRED_HUMAN_FIELDS = (
135
+ "implementationPlanning.selectedDirectionRef",
136
+ "implementationPlanning.directionRealization",
137
+ "implementationPlanning.coverageSummary",
138
+ "implementationPlanning.stageMap",
139
+ "implementationPlanning.requirementCoverage",
140
+ )
141
+
142
+ IMPLEMENTATION_PLANNING_INVALIDATED_REQUIRED_HUMAN_FIELDS = (
143
+ "implementationPlanning.selectedDirectionRef",
144
+ "implementationPlanning.directionInvalidation",
145
+ "implementationPlanning.routing",
146
+ )
147
+
148
+
149
+ def required_human_fields_for_report(task_type: str, data: dict) -> tuple[str, ...]:
150
+ if task_type != "implementation-planning":
151
+ return TASK_TYPE_REQUIRED_HUMAN_FIELDS.get(task_type, ())
152
+ planning = data.get("implementationPlanning", {})
153
+ if planning.get("planningContract") != "selected-direction":
154
+ return IMPLEMENTATION_PLANNING_LEGACY_REQUIRED_HUMAN_FIELDS
155
+ if planning.get("outcome") == "direction-invalidated":
156
+ return IMPLEMENTATION_PLANNING_INVALIDATED_REQUIRED_HUMAN_FIELDS
157
+ return IMPLEMENTATION_PLANNING_PLAN_READY_REQUIRED_HUMAN_FIELDS
158
+
127
159
 
128
160
  def report_property_for(task_type: str) -> str:
129
161
  try:
@@ -1,10 +1,10 @@
1
1
  """Phase 7 report post-processing — the single reference point.
2
2
 
3
3
  Phase 7 turns a Phase 6 final-report data.json into shippable artifacts through
4
- five ordered steps: English-SSOT verification, usage substitution, html view
5
- rendering, follow-up task spawning, and run validation. The order is
6
- load-bearing — rendering before substitution ships `--` token cells, and
7
- validating before rendering trips the report-views contract.
4
+ six ordered steps: activity projection, English-SSOT verification, usage
5
+ substitution, html view rendering, follow-up task spawning, and run validation.
6
+ The order is load-bearing — rendering before substitution ships `--` token
7
+ cells, and validating before rendering trips the report-views contract.
8
8
 
9
9
  The translation sidecar is NOT one of these steps. `render-views` overlays it,
10
10
  so a non-English run dispatches the translator before this sequence starts —
@@ -25,11 +25,13 @@ from dataclasses import dataclass
25
25
  from pathlib import Path
26
26
  from typing import Any, Callable, Mapping, Sequence
27
27
 
28
+ from .agent_activity import ActivityProjectionError, project_agent_activity
29
+ from .dispatch_state import DispatchError, link_agent_dispatch_result
28
30
  from .final_report_paths import final_report_data_path, final_report_markdown_path
29
31
  from .paths import task_dir, task_manifest_file
30
- from .dispatch_state import DispatchError, link_agent_dispatch_result
31
32
 
32
33
 
34
+ STEP_PROJECT_ACTIVITY = "project-activity"
33
35
  STEP_CHECK_SOURCE = "check-source"
34
36
  STEP_TOKEN_USAGE = "token-usage"
35
37
  STEP_RENDER_VIEWS = "render-views"
@@ -37,6 +39,7 @@ STEP_SPAWN_FOLLOWUPS = "spawn-followups"
37
39
  STEP_VALIDATE_RUN = "validate-run"
38
40
 
39
41
  STEP_ORDER = (
42
+ STEP_PROJECT_ACTIVITY,
40
43
  # First, because everything after it derives from the data.json: rendering
41
44
  # a Korean SSOT into English chrome, spawning follow-ups from it, and
42
45
  # validating it all succeed on a record the next phase cannot read.
@@ -235,6 +238,16 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
235
238
  """Assemble the ordered Phase 7 argv list. Order is contractual."""
236
239
  markdown_path = ctx.markdown_path
237
240
  return [
241
+ (
242
+ STEP_PROJECT_ACTIVITY,
243
+ [
244
+ "<in-process>",
245
+ "agent-activity",
246
+ "project",
247
+ str(ctx.manifest_path),
248
+ str(ctx.data_path),
249
+ ],
250
+ ),
238
251
  (
239
252
  STEP_CHECK_SOURCE,
240
253
  [
@@ -349,8 +362,8 @@ def run_finalize(
349
362
 
350
363
  ``only`` restricts the run to the named steps, preserving contractual
351
364
  order. The last step (`validate-run`) is the one that usually fails, and
352
- re-running the whole sequence to retry it repeats three idempotent steps
353
- at full token and wall-clock cost.
365
+ re-running the whole sequence to retry it repeats every preceding idempotent
366
+ step at full token and wall-clock cost.
354
367
  """
355
368
  steps: list[dict[str, Any]] = []
356
369
  try:
@@ -372,6 +385,29 @@ def run_finalize(
372
385
  for name, command in commands:
373
386
  if before_step is not None:
374
387
  before_step(name)
388
+ if name == STEP_PROJECT_ACTIVITY:
389
+ try:
390
+ rows = project_agent_activity(
391
+ ctx.project_root,
392
+ ctx.manifest_path,
393
+ ctx.data_path,
394
+ )
395
+ except ActivityProjectionError as exc:
396
+ result = subprocess.CompletedProcess(
397
+ command,
398
+ 1,
399
+ "",
400
+ str(exc),
401
+ )
402
+ else:
403
+ result = subprocess.CompletedProcess(
404
+ command,
405
+ 0,
406
+ json.dumps({"count": len(rows)}),
407
+ "",
408
+ )
409
+ else:
410
+ result = None
375
411
  if name == STEP_VALIDATE_RUN:
376
412
  try:
377
413
  _link_lead_result_for_validation(ctx)
@@ -381,12 +417,13 @@ def run_finalize(
381
417
  "reason": f"lead result linkage failed: {exc}",
382
418
  "steps": steps,
383
419
  }
384
- result = subprocess.run(
385
- command,
386
- cwd=ctx.project_root,
387
- text=True,
388
- capture_output=True,
389
- )
420
+ if result is None:
421
+ result = subprocess.run(
422
+ command,
423
+ cwd=ctx.project_root,
424
+ text=True,
425
+ capture_output=True,
426
+ )
390
427
  steps.append(step_payload(name, command, result))
391
428
  if result.returncode != 0:
392
429
  return {
@@ -489,7 +526,7 @@ def _parser() -> argparse.ArgumentParser:
489
526
  help=(
490
527
  "run only these steps (repeatable, contractual order preserved). "
491
528
  "Use `--only validate-run` to retry the step that usually fails "
492
- "without repeating the three idempotent ones before it."
529
+ "without repeating the preceding idempotent steps."
493
530
  ),
494
531
  )
495
532
  return parser
@@ -85,6 +85,7 @@ def _own_section_ids(data: dict, omitted_fields: tuple[str, ...] = ()) -> set[st
85
85
 
86
86
  found: set[str] = set()
87
87
  _collect_ids(data.get("clarificationItems", []), found)
88
+ _collect_ids(data.get("agentActivity", []), found)
88
89
  property_name = TASK_TYPE_DATA_PROPERTY.get(data.get("header", {}).get("taskType", ""))
89
90
  if property_name:
90
91
  block = data.get(property_name, {})
@@ -120,9 +121,10 @@ def evidence_index(data: dict) -> dict[str, object]:
120
121
 
121
122
  def _collect_ids(value: object, found: set[str]) -> None:
122
123
  if isinstance(value, dict):
123
- row_id = value.get("id")
124
- if isinstance(row_id, str) and row_id:
125
- found.add(row_id)
124
+ for key in ("id", "activityId"):
125
+ row_id = value.get(key)
126
+ if isinstance(row_id, str) and row_id:
127
+ found.add(row_id)
126
128
  for nested in value.values():
127
129
  _collect_ids(nested, found)
128
130
  elif isinstance(value, list):
@@ -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,26 +104,42 @@ _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)
109
+ activities = tuple(
110
+ row for row in data.get("agentActivity", []) if isinstance(row, dict)
111
+ )
112
+ decision_cards = tuple(
113
+ row
114
+ for row in data.get("clarificationItems", [])
115
+ if isinstance(row, dict)
116
+ and row.get("blocks") == "approval"
117
+ and isinstance(row.get("approvalContext"), dict)
118
+ )
94
119
  context = {
95
120
  "humanSummary": data["humanSummary"],
96
121
  "planning": planning,
97
- "narrative": planning["userNarrative"],
122
+ "narrative": planning.get("userNarrative", {}),
98
123
  "stageFigure": figure,
99
124
  "approval": approval,
100
125
  "openDecisions": [
101
126
  row
102
127
  for row in data.get("clarificationItems", [])
103
- if row.get("blocks") == "approval"
128
+ if isinstance(row, dict)
129
+ and row.get("blocks") == "approval"
104
130
  and row.get("status") in {"open", "answered"}
105
131
  ],
132
+ "agentActivities": activities,
133
+ "activityById": {
134
+ row["activityId"]: row for row in activities if row.get("activityId")
135
+ },
136
+ "decisionCards": decision_cards,
106
137
  "evidenceIndex": evidence_index(data),
107
138
  }
108
139
  return HumanReportView(
109
140
  "implementation-planning",
110
141
  "html/tasks/implementation-planning.template.html",
111
142
  context,
112
- (figure,),
143
+ (figure,) if figure is not None else (),
113
144
  _OMITTED_FIELDS,
114
145
  )
@@ -125,8 +125,10 @@ PROSE_KEYS = frozenset({
125
125
  "topic",
126
126
  "trigger",
127
127
  "triggerSignal",
128
+ "unblockCondition",
128
129
  "userChoice",
129
130
  "userInput",
131
+ "userText",
130
132
  "verification",
131
133
  "verificationMethod",
132
134
  "verificationSignal",
@@ -138,6 +140,8 @@ PROSE_KEYS = frozenset({
138
140
  # that must stay verbatim to be evidence at all.
139
141
  STRUCTURAL_KEYS = frozenset({
140
142
  "action", # implementation.py: _FILE_ACTIONS lookup key
143
+ "activityId",
144
+ "activityIds",
141
145
  "adapter",
142
146
  "affectedId",
143
147
  "affectedTarget",
@@ -152,12 +156,15 @@ STRUCTURAL_KEYS = frozenset({
152
156
  "capturedHeadSha",
153
157
  "carriedForwardFromSeq",
154
158
  "carryJson",
159
+ "checkRefs",
155
160
  "clarificationId",
161
+ "classification",
156
162
  "claudeCode",
157
163
  "collectorBranch",
158
164
  "command",
159
165
  "commandOrObservation",
160
166
  "commandOrTest",
167
+ "commands",
161
168
  "commitListQuote",
162
169
  "complexity",
163
170
  "componentId",
@@ -165,14 +172,18 @@ STRUCTURAL_KEYS = frozenset({
165
172
  "conformanceTests",
166
173
  "coveredBy",
167
174
  "createdAt",
175
+ "cwd",
168
176
  "cycle",
169
177
  "dataStore",
170
178
  "date",
171
179
  "dependsOn",
172
180
  "diffSummaryQuote",
173
181
  "direction",
182
+ "disposition",
174
183
  "dispatches",
175
184
  "executorWorktreePath",
185
+ "evidenceRefs",
186
+ "exitCode",
176
187
  "existingPrUrl",
177
188
  "externalSystem",
178
189
  "featureId",
@@ -215,6 +226,7 @@ STRUCTURAL_KEYS = frozenset({
215
226
  "ownerComponentId",
216
227
  "path",
217
228
  "planFile",
229
+ "planItemIds",
218
230
  "planStep",
219
231
  "prepItemId",
220
232
  "project",
@@ -223,11 +235,13 @@ STRUCTURAL_KEYS = frozenset({
223
235
  "rawStat",
224
236
  "readBoundary",
225
237
  "readOnlyCommandLog",
238
+ "recommendedDisposition",
226
239
  "relatedIds",
227
240
  "relation",
228
241
  "replacedInTest",
229
242
  "replacedWith",
230
243
  "reportPath",
244
+ "resultPath",
231
245
  "requestKind",
232
246
  "requestPath",
233
247
  "requestVerbatim", # the user's own words, quoted as evidence