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.
- package/README.md +8 -6
- package/docs/architecture/storage-model.md +24 -3
- package/docs/architecture.md +21 -35
- package/docs/cli.md +39 -7
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/performance-improvement-plan-v2.md +6 -5
- package/docs/project-structure-overview.md +33 -25
- package/docs/task-process/README.md +6 -4
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +2 -2
- package/docs/task-process/implementation-option-selection.md +70 -0
- package/docs/task-process/implementation-planning.md +24 -16
- package/docs/task-process/requirements-discovery.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +30 -6
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +2 -1
- package/runtime/bin/lib/okstra/usage.sh +3 -0
- package/runtime/bin/okstra-provider-exec.py +29 -12
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/duties/direction-selection-worker.md +44 -0
- package/runtime/prompts/duties/planning-worker.md +12 -4
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +5 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
- package/runtime/prompts/lead/plan-body-verification.md +23 -6
- package/runtime/prompts/lead/report-writer.md +33 -11
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +4 -4
- package/runtime/prompts/profiles/final-verification.md +3 -3
- package/runtime/prompts/profiles/forbidden-actions.json +7 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
- package/runtime/prompts/profiles/implementation-planning.md +61 -46
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +9 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
- package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
- package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
- package/runtime/python/okstra_ctl/doctor.py +2 -2
- package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
- package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
- package/runtime/python/okstra_ctl/implementation_options.py +479 -0
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/plan_items.py +51 -3
- package/runtime/python/okstra_ctl/render.py +1 -0
- package/runtime/python/okstra_ctl/render_final_report.py +16 -19
- package/runtime/python/okstra_ctl/report_contract.py +45 -14
- package/runtime/python/okstra_ctl/report_finalize.py +68 -9
- package/runtime/python/okstra_ctl/report_html/render.py +4 -2
- package/runtime/python/okstra_ctl/report_html/router.py +4 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
- package/runtime/python/okstra_ctl/report_views.py +148 -12
- package/runtime/python/okstra_ctl/run.py +393 -4
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +16 -10
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/user_response.py +75 -0
- package/runtime/python/okstra_ctl/wizard.py +144 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/worker_request.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +29 -7
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/reports/final-report-v2.template.md +4 -0
- package/runtime/templates/reports/final-verification-input.template.md +1 -1
- package/runtime/templates/reports/html/base.template.html +3 -2
- package/runtime/templates/reports/html/i18n/en.json +21 -1
- package/runtime/templates/reports/html/i18n/ko.json +21 -1
- package/runtime/templates/reports/html/macros/forms.html +21 -2
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
- package/runtime/templates/reports/i18n/en.json +13 -0
- package/runtime/templates/reports/implementation-input.template.md +4 -2
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
- package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
- package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
- package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
- package/runtime/templates/reports/report.js +111 -4
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/templates/reports/task-brief.template.md +9 -3
- package/runtime/templates/reports/user-response.template.md +25 -4
- package/runtime/templates/worker-prompt-preamble.md +8 -0
- package/runtime/validators/lib/fixtures.sh +49 -17
- package/runtime/validators/validate-implementation-plan-stages.py +169 -4
- package/runtime/validators/validate-report-views.py +2 -2
- package/runtime/validators/validate-run.py +149 -498
- package/runtime/validators/validate_improvement_report.py +5 -1
- package/runtime/validators/validate_session_conformance.py +1 -1
- package/src/cli-registry.mjs +8 -1
- package/src/commands/execute/codex-run.mjs +1 -0
- package/src/commands/execute/render-bundle.mjs +1 -0
- package/src/commands/execute/team.mjs +3 -3
- package/src/commands/execute/worktree-status.mjs +109 -0
- package/src/commands/lifecycle/install.mjs +0 -2
- package/src/commands/report/finalize.mjs +13 -6
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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[
|
|
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
|
-
|
|
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
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
621
|
+
recovery = _recovery_step_names(result)
|
|
622
|
+
if recovery:
|
|
623
|
+
flags = " ".join(f"--only {name}" for name in recovery)
|
|
564
624
|
print(
|
|
565
|
-
"
|
|
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
|
|
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
|
)
|