okstra 0.172.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.
- package/README.md +8 -6
- package/docs/architecture/storage-model.md +11 -0
- package/docs/architecture.md +16 -14
- package/docs/cli.md +36 -5
- package/docs/performance-improvement-plan-v2.md +6 -5
- package/docs/project-structure-overview.md +21 -13
- package/docs/task-process/README.md +5 -3
- 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 +23 -15
- package/docs/task-process/requirements-discovery.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- 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 +1 -0
- package/runtime/bin/lib/okstra/usage.sh +3 -0
- 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/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +5 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
- package/runtime/prompts/lead/plan-body-verification.md +20 -3
- package/runtime/prompts/lead/report-writer.md +27 -5
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +3 -3
- 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 +50 -38
- package/runtime/prompts/profiles/implementation.md +2 -1
- package/runtime/prompts/profiles/improvement-discovery.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/agent_invocation.py +1 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
- package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
- 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/plan_items.py +51 -3
- package/runtime/python/okstra_ctl/render.py +1 -0
- package/runtime/python/okstra_ctl/render_final_report.py +1 -0
- package/runtime/python/okstra_ctl/report_contract.py +45 -13
- 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 +350 -2
- package/runtime/python/okstra_ctl/scope_provenance.py +15 -9
- 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/workflow.py +29 -7
- package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
- 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/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/validate-implementation-plan-stages.py +106 -1
- package/runtime/validators/validate-report-views.py +2 -2
- package/runtime/validators/validate-run.py +135 -25
- package/runtime/validators/validate_improvement_report.py +5 -1
- package/src/commands/execute/codex-run.mjs +1 -0
- package/src/commands/execute/render-bundle.mjs +1 -0
|
@@ -60,9 +60,14 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
60
60
|
- What is the visible symptom?
|
|
61
61
|
- What are the expected vs actual results?
|
|
62
62
|
- What are the current hypotheses and missing evidence?
|
|
63
|
+
- If `Task Type` is `implementation-option-selection`:
|
|
64
|
+
- Is this a comparison among possible directions or validation of one preselected direction?
|
|
65
|
+
- Which stable `EB-NNN`, `PB-NNN`, and `EO-NNN` requirements form the coverage denominator?
|
|
66
|
+
- Which mechanism, architecture boundary, preservation rule, or implementation constraint must each direction address?
|
|
63
67
|
- If `Task Type` is `implementation-planning`:
|
|
64
|
-
-
|
|
65
|
-
-
|
|
68
|
+
- Which validated selection report and `selectedDirectionRef` authorise this new plan?
|
|
69
|
+
- Which mechanism, architecture boundary, invariants, and user constraints must the plan preserve?
|
|
70
|
+
- Which dependencies or migrations matter while realizing that one direction?
|
|
66
71
|
- What validation and rollback approach is expected?
|
|
67
72
|
- If `Task Type` is `implementation`:
|
|
68
73
|
- Which approved `implementation-planning` final report authorises this run, and is its frontmatter `approved: true` cited verbatim?
|
|
@@ -141,6 +146,7 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
141
146
|
- Allowed and forbidden actions for each task type are listed in `Lifecycle Phase Boundaries` of the okstra skill (`prompts/lead/okstra-lead-contract.md`). The lead and every worker stay inside that boundary.
|
|
142
147
|
- "proceed to the next step" or any equivalent user phrase is interpreted as "complete the remaining outputs of the current phase," never as "start the next lifecycle phase." The next phase begins only via a fresh okstra invocation with the new `--task-type`.
|
|
143
148
|
- For `implementation-planning` specifically: produce a plan document with the sections listed in `okstra-implementation-planning-input.template.md` `## Required Plan Deliverable`. Do not edit project source code, run builds/migrations/deployments, or write artifacts outside the run's own directories.
|
|
149
|
+
- For `implementation-option-selection` specifically: compare or validate implementation directions without editing source, running tests/builds, or writing exact file lists, stage maps, and test commands. Only directions with 100% requirement coverage and 100% scope precision may be displayed.
|
|
144
150
|
- For `implementation` specifically: edits are bounded by the approved plan's file list (the `--approved-plan` reference). The run MUST refuse to start if the approved plan path is missing or its frontmatter `approved` field is not `true`. `git push`, publish, deploy, real migrations, and any third-party write API call remain forbidden; only local `git add`/`git commit` are allowed. Verifier roles stay read-only — they record fix recommendations rather than applying edits — and acceptance verdicts belong to `final-verification`, not this phase.
|
|
145
151
|
|
|
146
152
|
## Available MCP Servers
|
|
@@ -157,7 +163,7 @@ How to invoke (worker-by-worker):
|
|
|
157
163
|
|
|
158
164
|
Usage policy:
|
|
159
165
|
|
|
160
|
-
- **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
|
|
166
|
+
- **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
|
|
161
167
|
- **`implementation` phase**: read-only MCP queries are permitted as cross-checks; MCP MUST NOT be used as a write path even if a write tool becomes available — schema/data mutations belong in the codebase migration files reviewed by humans.
|
|
162
168
|
- **CLI-wrapper workers**: can use these MCP servers only if their provider CLI configuration mirrors the same servers. If not configured, the worker should record `MCP not available in this CLI` in `Missing Information or Assumptions` rather than guessing.
|
|
163
169
|
- **Forbidden**: connecting to non-listed databases, running anything that mutates state (server is read-only — flagged write attempts are a contract violation), persisting query results outside the run's own artifact directories.
|
|
@@ -9,9 +9,11 @@ This file defines the standard format of the markdown produced by the **Export u
|
|
|
9
9
|
|
|
10
10
|
```yaml
|
|
11
11
|
task-key: <task-group>/<task-id>
|
|
12
|
-
task-type: <requirements-discovery | error-analysis | implementation-planning | implementation | final-verification | release-handoff | project-analysis | feature-analysis | change-impact-analysis>
|
|
12
|
+
task-type: <requirements-discovery | error-analysis | implementation-option-selection | implementation-planning | implementation | final-verification | release-handoff | improvement-discovery | project-analysis | feature-analysis | change-impact-analysis>
|
|
13
13
|
seq: <3-digit zero-padded run sequence>
|
|
14
14
|
source-report: <project-relative path to the final-report .md the HTML was derived from>
|
|
15
|
+
source-data: <project-relative path to the final-report data.json; omit for legacy reports without one>
|
|
16
|
+
source-data-sha256: <SHA-256 of the exact source-data bytes; omit when source-data is omitted>
|
|
15
17
|
created-by: user
|
|
16
18
|
created-at: <ISO 8601 UTC timestamp>
|
|
17
19
|
```
|
|
@@ -72,15 +74,16 @@ When you pick a verdict in the Plan Decision widget and Export, the following bl
|
|
|
72
74
|
```markdown
|
|
73
75
|
## PLAN DECISION
|
|
74
76
|
- Status: <approved | revision-requested | rejected>
|
|
75
|
-
- Implementation-Option: <the name exactly as in Option Candidates>
|
|
77
|
+
- Implementation-Option: <legacy plans only: the name exactly as in Option Candidates>
|
|
76
78
|
- Reason:
|
|
77
79
|
> <one quoted line per input line>
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
- `revision-requested` and `rejected` require a reason: a plan sent back without one leaves the next run guessing at what to change, so Export refuses to serialise it.
|
|
81
|
-
- `
|
|
83
|
+
- A `planningContract: selected-direction` plan never emits `Implementation-Option:`. The direction was confirmed before planning, and this block decides only whether the detailed plan is approved, revised, or rejected.
|
|
84
|
+
- A legacy candidate plan emits `Implementation-Option:` only for `approved`, and only when you moved off the recommended default. Otherwise implementation falls back to the legacy plan's Recommended Option.
|
|
82
85
|
- `Reason:` is optional for `approved`, and the line is omitted when empty.
|
|
83
|
-
- The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies
|
|
86
|
+
- The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies `--approve`; it applies `--implementation-option` only for a legacy plan. The sidecar itself does not bypass approval-gate validation. A non-approved status is ignored there — that step only ever looks for an approval.
|
|
84
87
|
- The parser is `parse_plan_decision` in `scripts/okstra_ctl/user_response.py`, and it accepts only the lowercase statuses that are byte-identical to the producer output (hand-edited values such as `Approved` are rejected fail-closed).
|
|
85
88
|
- `--resume-clarification` attaches the sidecar verbatim, so the PLAN DECISION block reaches the next planning run — which is how a rejection and its reason get acted on.
|
|
86
89
|
|
|
@@ -102,6 +105,24 @@ The Analysis Review control appends one decision block. `revision-requested` and
|
|
|
102
105
|
|
|
103
106
|
The block is the review decision's sole storage location. Export never changes the source final-report markdown. A later analysis rerun receives the sidecar through `--clarification-response` and records one `analysisReviewResolution` row for each imported affected ID.
|
|
104
107
|
|
|
108
|
+
## DIRECTION SELECTION block (implementation-option-selection comparison only)
|
|
109
|
+
|
|
110
|
+
Selecting one ranked direction and checking `Confirmed` appends this block. `Selection-Note` and `Constraints` keep their field lines when empty, but do not emit empty quote lines.
|
|
111
|
+
|
|
112
|
+
```markdown
|
|
113
|
+
## DIRECTION SELECTION
|
|
114
|
+
- Status: selected
|
|
115
|
+
- Option-ID: IO-002
|
|
116
|
+
- Option-Name: Adapter boundary
|
|
117
|
+
- Confirmed: true
|
|
118
|
+
- Selection-Note:
|
|
119
|
+
> Use the existing port.
|
|
120
|
+
- Constraints:
|
|
121
|
+
> Keep PB-001 unchanged.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The parser accepts exactly one block, lowercase `selected`, and lowercase `true`. A `preselected-validation` report displays its confirmed upstream direction read-only and does not export a new selection block.
|
|
125
|
+
|
|
105
126
|
## Compatibility Rules
|
|
106
127
|
|
|
107
128
|
- If `Kind` has an unknown value, the form renders with a `<textarea>` fallback, and the received `Kind` string is preserved as-is during serialization.
|
|
@@ -61,6 +61,14 @@ Every analysis result starts with YAML frontmatter containing the task identity
|
|
|
61
61
|
|
|
62
62
|
Every item has a worker-local ID and file:line evidence where code evidence exists. Sections 1–5 are the common core: feasibility, requirement interpretation, hidden assumptions, alternatives, and execution risk. Section 6 is the only legal home for specialization and is not consensus input.
|
|
63
63
|
|
|
64
|
+
### Selected-direction planning ownership
|
|
65
|
+
|
|
66
|
+
For `implementation-planning` with `selected-direction.json`, sections 1–5 read that snapshot and the original requirements, then assess only its realization into files, interfaces, stages, validation, rollback, bidirectional requirement links, and `direction-invalidated` evidence. Direction selection remains upstream of this prompt.
|
|
67
|
+
|
|
68
|
+
### Legacy candidate-comparison ownership
|
|
69
|
+
|
|
70
|
+
For an implementation-planning compatibility rerun without the snapshot, sections 1–5 retain candidate comparison, trade-off, recommendation, and `P-Opt-*` evidence.
|
|
71
|
+
|
|
64
72
|
### Ticket Tagging
|
|
65
73
|
|
|
66
74
|
For `requirements-discovery`, `error-analysis`, and `implementation-planning`, tag every section 1–5 item with its related ticket. Use `Issue / Ticket`, fall back to Task ID, then `unknown`; comma-separate multiple tickets.
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import argparse
|
|
9
9
|
import re
|
|
10
10
|
import sys
|
|
11
|
+
from collections import Counter
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import List, Tuple
|
|
@@ -472,6 +473,99 @@ def _check_data_step_counts(
|
|
|
472
473
|
return errs
|
|
473
474
|
|
|
474
475
|
|
|
476
|
+
def _check_data_stage_identities(
|
|
477
|
+
raw_stage_map: List[dict], stages: List[dict]
|
|
478
|
+
) -> List[ValidationError]:
|
|
479
|
+
"""Reject ambiguous schema-v2 identities before building stage indexes."""
|
|
480
|
+
errs: List[ValidationError] = []
|
|
481
|
+
stage_map_numbers = [
|
|
482
|
+
row["stage"]
|
|
483
|
+
for row in raw_stage_map
|
|
484
|
+
if isinstance(row, dict) and isinstance(row.get("stage"), int)
|
|
485
|
+
]
|
|
486
|
+
stage_numbers = [
|
|
487
|
+
row["stage"]
|
|
488
|
+
for row in stages
|
|
489
|
+
if isinstance(row, dict) and isinstance(row.get("stage"), int)
|
|
490
|
+
]
|
|
491
|
+
duplicate_stage_map = {
|
|
492
|
+
number for number, count in Counter(stage_map_numbers).items() if count > 1
|
|
493
|
+
}
|
|
494
|
+
duplicate_stages = {
|
|
495
|
+
number for number, count in Counter(stage_numbers).items() if count > 1
|
|
496
|
+
}
|
|
497
|
+
errs.extend(
|
|
498
|
+
ValidationError("S3", number, f"stageMap duplicate stage {number}")
|
|
499
|
+
for number in sorted(duplicate_stage_map)
|
|
500
|
+
)
|
|
501
|
+
errs.extend(
|
|
502
|
+
ValidationError("S3", number, f"stages[] duplicate stage {number}")
|
|
503
|
+
for number in sorted(duplicate_stages)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
stage_map_set = set(stage_map_numbers)
|
|
507
|
+
stage_set = set(stage_numbers)
|
|
508
|
+
errs.extend(
|
|
509
|
+
ValidationError(
|
|
510
|
+
"S3",
|
|
511
|
+
number,
|
|
512
|
+
f"stageMap is missing stages[] stage {number}",
|
|
513
|
+
)
|
|
514
|
+
for number in sorted(stage_set - stage_map_set)
|
|
515
|
+
)
|
|
516
|
+
errs.extend(
|
|
517
|
+
ValidationError(
|
|
518
|
+
"S3",
|
|
519
|
+
number,
|
|
520
|
+
f"stages[] is missing stageMap stage {number}",
|
|
521
|
+
)
|
|
522
|
+
for number in sorted(stage_map_set - stage_set)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
stage_map_by_number = {
|
|
526
|
+
row["stage"]: row
|
|
527
|
+
for row in raw_stage_map
|
|
528
|
+
if isinstance(row, dict)
|
|
529
|
+
and isinstance(row.get("stage"), int)
|
|
530
|
+
and row["stage"] not in duplicate_stage_map
|
|
531
|
+
}
|
|
532
|
+
stages_by_number = {
|
|
533
|
+
row["stage"]: row
|
|
534
|
+
for row in stages
|
|
535
|
+
if isinstance(row, dict)
|
|
536
|
+
and isinstance(row.get("stage"), int)
|
|
537
|
+
and row["stage"] not in duplicate_stages
|
|
538
|
+
}
|
|
539
|
+
errs.extend(
|
|
540
|
+
ValidationError(
|
|
541
|
+
"S3",
|
|
542
|
+
number,
|
|
543
|
+
f"stageMap and stages[] stage {number} title must match",
|
|
544
|
+
)
|
|
545
|
+
for number in sorted(stage_map_by_number.keys() & stages_by_number.keys())
|
|
546
|
+
if stage_map_by_number[number].get("title")
|
|
547
|
+
!= stages_by_number[number].get("title")
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
for stage in stages:
|
|
551
|
+
number = stage.get("stage")
|
|
552
|
+
step_numbers = [
|
|
553
|
+
step["step"]
|
|
554
|
+
for step in stage.get("stepwiseExecution") or []
|
|
555
|
+
if isinstance(step, dict) and isinstance(step.get("step"), int)
|
|
556
|
+
]
|
|
557
|
+
errs.extend(
|
|
558
|
+
ValidationError(
|
|
559
|
+
"S4",
|
|
560
|
+
number if isinstance(number, int) else 0,
|
|
561
|
+
f"stages[] stage {number} step {step} is duplicate",
|
|
562
|
+
)
|
|
563
|
+
for step, count in sorted(Counter(step_numbers).items())
|
|
564
|
+
if count > 1
|
|
565
|
+
)
|
|
566
|
+
return errs
|
|
567
|
+
|
|
568
|
+
|
|
475
569
|
def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
476
570
|
"""The S-checks that schema v2 cannot express, over `implementationPlanning`.
|
|
477
571
|
|
|
@@ -482,11 +576,22 @@ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
|
482
576
|
covers presence and cardinality; this covers the relationships between
|
|
483
577
|
fields, which is what a JSON Schema has no way to say.
|
|
484
578
|
"""
|
|
485
|
-
|
|
579
|
+
if (
|
|
580
|
+
planning.get("planningContract") == "selected-direction"
|
|
581
|
+
and planning.get("outcome") == "direction-invalidated"
|
|
582
|
+
):
|
|
583
|
+
return []
|
|
584
|
+
|
|
585
|
+
raw_stage_map = planning.get("stageMap") or []
|
|
586
|
+
stage_map, errors = _data_stage_metas(raw_stage_map)
|
|
486
587
|
stages = [s for s in (planning.get("stages") or []) if isinstance(s, dict)]
|
|
487
588
|
if not stage_map and not stages:
|
|
488
589
|
return errors
|
|
489
590
|
|
|
591
|
+
identity_errors = _check_data_stage_identities(raw_stage_map, stages)
|
|
592
|
+
errors.extend(identity_errors)
|
|
593
|
+
if identity_errors:
|
|
594
|
+
return errors
|
|
490
595
|
errors.extend(_check_data_step_counts(stage_map, stages))
|
|
491
596
|
errors.extend(_check_depends_on(stage_map))
|
|
492
597
|
errors.extend(_report_shared_parallel_files({
|
|
@@ -43,7 +43,7 @@ from okstra_ctl.report_views import ( # noqa: E402
|
|
|
43
43
|
)
|
|
44
44
|
from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
|
|
45
45
|
from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
|
|
46
|
-
from okstra_ctl.report_contract import
|
|
46
|
+
from okstra_ctl.report_contract import required_human_fields_for_report # noqa: E402
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
_EXTERNAL_URL_RE = re.compile(
|
|
@@ -98,7 +98,7 @@ def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
|
|
|
98
98
|
if f'data-task-template="{task_type}"' not in html_text:
|
|
99
99
|
failures.append(f"v2 html task template mismatch for {task_type}")
|
|
100
100
|
actual_fields = set(re.findall(r'data-report-field="([^"]+)"', main_body))
|
|
101
|
-
for field in
|
|
101
|
+
for field in required_human_fields_for_report(task_type, data):
|
|
102
102
|
if field not in actual_fields:
|
|
103
103
|
failures.append(f"missing human field: {field}")
|
|
104
104
|
|
|
@@ -71,7 +71,12 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
|
|
|
71
71
|
coverage_row_blocked_on,
|
|
72
72
|
stages_for_clarification,
|
|
73
73
|
)
|
|
74
|
-
from okstra_ctl.workflow import
|
|
74
|
+
from okstra_ctl.workflow import ( # noqa: E402
|
|
75
|
+
DEFAULT_NEXT_PHASE,
|
|
76
|
+
ERROR_ANALYSIS_ROUTING_DIRECTIONS,
|
|
77
|
+
PHASE_SEQUENCE,
|
|
78
|
+
REQUIREMENTS_DISCOVERY_ROUTING_TARGETS,
|
|
79
|
+
)
|
|
75
80
|
from okstra_ctl.md_table import ( # noqa: E402
|
|
76
81
|
is_separator_row as _is_markdown_separator,
|
|
77
82
|
split_pipe_row as _split_pipe_row,
|
|
@@ -80,9 +85,16 @@ from okstra_ctl.final_report_paths import final_report_data_path as _data_path_f
|
|
|
80
85
|
from okstra_ctl.improvement_assignment import ( # noqa: E402
|
|
81
86
|
validate_primary_lens_assignments,
|
|
82
87
|
)
|
|
88
|
+
from okstra_ctl.implementation_options import ( # noqa: E402
|
|
89
|
+
validate_implementation_option_selection,
|
|
90
|
+
)
|
|
91
|
+
from okstra_ctl.implementation_direction import ( # noqa: E402
|
|
92
|
+
validate_selected_direction_plan,
|
|
93
|
+
)
|
|
83
94
|
from okstra_ctl.worker_prompt_policy import GRILLING_LOG_HEADER # noqa: E402
|
|
84
95
|
from okstra_ctl.scope_provenance import ( # noqa: E402
|
|
85
96
|
brief_citation_problem,
|
|
97
|
+
brief_end_state_id_sequence,
|
|
86
98
|
brief_end_state_ids,
|
|
87
99
|
brief_headings,
|
|
88
100
|
parse_source,
|
|
@@ -406,7 +418,28 @@ def _error_analysis_next_phase(data: Mapping[str, Any]) -> str | None:
|
|
|
406
418
|
if not isinstance(routing, Mapping):
|
|
407
419
|
return None
|
|
408
420
|
target = routing.get("nextTaskType")
|
|
409
|
-
if target in
|
|
421
|
+
if target in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
|
|
422
|
+
return str(target)
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _requirements_discovery_next_phase(data: Mapping[str, Any]) -> str | None:
|
|
427
|
+
if not isinstance(data, Mapping):
|
|
428
|
+
return None
|
|
429
|
+
header = data.get("header")
|
|
430
|
+
if (
|
|
431
|
+
not isinstance(header, Mapping)
|
|
432
|
+
or header.get("taskType") != "requirements-discovery"
|
|
433
|
+
):
|
|
434
|
+
return None
|
|
435
|
+
requirements = data.get("requirementsDiscovery")
|
|
436
|
+
if not isinstance(requirements, Mapping):
|
|
437
|
+
return None
|
|
438
|
+
routing = requirements.get("routing")
|
|
439
|
+
if not isinstance(routing, Mapping):
|
|
440
|
+
return None
|
|
441
|
+
target = routing.get("nextTaskType")
|
|
442
|
+
if target in REQUIREMENTS_DISCOVERY_ROUTING_TARGETS:
|
|
410
443
|
return str(target)
|
|
411
444
|
return None
|
|
412
445
|
|
|
@@ -445,11 +478,12 @@ def update_workflow_metadata(
|
|
|
445
478
|
# Validation just passed → actively advance to the next phase in
|
|
446
479
|
# the sequence rather than preserving a stale value that may equal
|
|
447
480
|
# current_phase (which would cause the lifecycle pointer to stall).
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
481
|
+
if current_phase == "requirements-discovery":
|
|
482
|
+
report_next_phase = _requirements_discovery_next_phase(report_data or {})
|
|
483
|
+
elif current_phase == "error-analysis":
|
|
484
|
+
report_next_phase = _error_analysis_next_phase(report_data or {})
|
|
485
|
+
else:
|
|
486
|
+
report_next_phase = None
|
|
453
487
|
next_recommended_phase = report_next_phase or advance_next_phase(
|
|
454
488
|
current_phase, phase_sequence
|
|
455
489
|
)
|
|
@@ -3142,10 +3176,15 @@ def _validate_error_analysis_consistency(
|
|
|
3142
3176
|
target = routing.get("nextTaskType")
|
|
3143
3177
|
leading_cause_id = routing.get("leadingCauseId")
|
|
3144
3178
|
candidate_id_set = set(candidate_ids)
|
|
3145
|
-
if target
|
|
3179
|
+
if isinstance(target, str) and target not in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
|
|
3180
|
+
failures.append(
|
|
3181
|
+
"final-report data.json: errorAnalysis.routing has unsupported "
|
|
3182
|
+
f"routing target `{target}`."
|
|
3183
|
+
)
|
|
3184
|
+
if target == "implementation-option-selection":
|
|
3146
3185
|
if not candidates:
|
|
3147
3186
|
failures.append(
|
|
3148
|
-
"final-report data.json: implementation-
|
|
3187
|
+
"final-report data.json: implementation-option-selection routing requires "
|
|
3149
3188
|
"at least one cause candidate."
|
|
3150
3189
|
)
|
|
3151
3190
|
if (
|
|
@@ -3153,7 +3192,7 @@ def _validate_error_analysis_consistency(
|
|
|
3153
3192
|
or leading_cause_id not in candidate_id_set
|
|
3154
3193
|
):
|
|
3155
3194
|
failures.append(
|
|
3156
|
-
"final-report data.json: implementation-
|
|
3195
|
+
"final-report data.json: implementation-option-selection routing "
|
|
3157
3196
|
"leadingCauseId must reference a cause candidate."
|
|
3158
3197
|
)
|
|
3159
3198
|
elif target == "error-analysis" and (
|
|
@@ -3165,10 +3204,7 @@ def _validate_error_analysis_consistency(
|
|
|
3165
3204
|
"empty or reference a cause candidate."
|
|
3166
3205
|
)
|
|
3167
3206
|
|
|
3168
|
-
expected_direction =
|
|
3169
|
-
"implementation-planning": "begin-planning",
|
|
3170
|
-
"error-analysis": "continue-investigation",
|
|
3171
|
-
}.get(target)
|
|
3207
|
+
expected_direction = ERROR_ANALYSIS_ROUTING_DIRECTIONS.get(target)
|
|
3172
3208
|
verdict_card_value = data.get("verdictCard")
|
|
3173
3209
|
verdict_card = (
|
|
3174
3210
|
verdict_card_value if isinstance(verdict_card_value, Mapping) else {}
|
|
@@ -3230,7 +3266,7 @@ def _validate_error_analysis_consistency(
|
|
|
3230
3266
|
|
|
3231
3267
|
if isinstance(target, str) and target in {
|
|
3232
3268
|
"error-analysis",
|
|
3233
|
-
"implementation-
|
|
3269
|
+
"implementation-option-selection",
|
|
3234
3270
|
}:
|
|
3235
3271
|
for field_name, value in (
|
|
3236
3272
|
("verdictCard.nextStep", verdict_card.get("nextStep")),
|
|
@@ -3373,7 +3409,25 @@ def validate_final_report_data(
|
|
|
3373
3409
|
|
|
3374
3410
|
task_type = (data.get("header") or {}).get("taskType")
|
|
3375
3411
|
_validate_verifier_fail_blocks_verdict(data, failures)
|
|
3376
|
-
if task_type == "implementation":
|
|
3412
|
+
if task_type == "implementation-option-selection":
|
|
3413
|
+
selection = data.get("implementationOptionSelection") or {}
|
|
3414
|
+
validation_root = project_root or report_path.parent
|
|
3415
|
+
original_ids = brief_end_state_id_sequence(
|
|
3416
|
+
_brief_path_from_manifest(manifest, validation_root)
|
|
3417
|
+
)
|
|
3418
|
+
roster = manifest.get("recommendedWorkers") or ()
|
|
3419
|
+
participating_analysers = tuple(
|
|
3420
|
+
worker for worker in roster if worker != "report-writer"
|
|
3421
|
+
)
|
|
3422
|
+
failures.extend(
|
|
3423
|
+
f"implementation-option-selection: {error}"
|
|
3424
|
+
for error in validate_implementation_option_selection(
|
|
3425
|
+
selection,
|
|
3426
|
+
original_ids,
|
|
3427
|
+
participating_analysers,
|
|
3428
|
+
)
|
|
3429
|
+
)
|
|
3430
|
+
elif task_type == "implementation":
|
|
3377
3431
|
_validate_stage_carry_sidecar_exists(data, report_path, failures)
|
|
3378
3432
|
if task_type == "error-analysis":
|
|
3379
3433
|
_validate_error_analysis_consistency(data, failures)
|
|
@@ -3382,6 +3436,31 @@ def validate_final_report_data(
|
|
|
3382
3436
|
_validate_verified_row_recorded(data, report_path, failures)
|
|
3383
3437
|
elif task_type == "implementation-planning":
|
|
3384
3438
|
active_report_contracts = report_contracts or set()
|
|
3439
|
+
planning = data.get("implementationPlanning") or {}
|
|
3440
|
+
selected_direction_contract = (
|
|
3441
|
+
isinstance(planning, Mapping)
|
|
3442
|
+
and planning.get("planningContract") == "selected-direction"
|
|
3443
|
+
)
|
|
3444
|
+
if selected_direction_contract:
|
|
3445
|
+
validation_root = project_root or report_path.parent
|
|
3446
|
+
try:
|
|
3447
|
+
brief_path = _brief_path_from_manifest(manifest, validation_root)
|
|
3448
|
+
except (OSError, ValueError) as exc:
|
|
3449
|
+
failures.append(
|
|
3450
|
+
"implementation-planning selected-direction: run manifest "
|
|
3451
|
+
f"taskBriefPath is malformed: {exc}"
|
|
3452
|
+
)
|
|
3453
|
+
brief_path = validation_root / "__invalid-brief__"
|
|
3454
|
+
task_root = _task_root_from_run_dir(report_path.parent.parent)
|
|
3455
|
+
snapshot_path = task_root / "instruction-set" / "selected-direction.json"
|
|
3456
|
+
failures.extend(
|
|
3457
|
+
f"implementation-planning selected-direction: {error}"
|
|
3458
|
+
for error in validate_selected_direction_plan(
|
|
3459
|
+
data, brief_path, snapshot_path
|
|
3460
|
+
)
|
|
3461
|
+
)
|
|
3462
|
+
if planning.get("outcome") == "direction-invalidated":
|
|
3463
|
+
return data
|
|
3385
3464
|
_validate_implementation_planning_cross_project(data, failures)
|
|
3386
3465
|
_validate_implementation_planning_decision_drafts(data, failures)
|
|
3387
3466
|
for warning in validate_plan_body_section(data, report_path, failures):
|
|
@@ -3401,8 +3480,9 @@ def validate_final_report_data(
|
|
|
3401
3480
|
resolve_architecture(_project_root_from_report(report_path)),
|
|
3402
3481
|
failures,
|
|
3403
3482
|
)
|
|
3404
|
-
|
|
3405
|
-
|
|
3483
|
+
if not selected_direction_contract:
|
|
3484
|
+
_validate_requirement_deviations(data, failures)
|
|
3485
|
+
_validate_requirement_coverage_covered_by(data, failures)
|
|
3406
3486
|
warnings = _validate_design_prep_contract(
|
|
3407
3487
|
data,
|
|
3408
3488
|
report_path,
|
|
@@ -7803,6 +7883,13 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
|
|
|
7803
7883
|
)
|
|
7804
7884
|
|
|
7805
7885
|
|
|
7886
|
+
_FINAL_VERIFICATION_ROUTING_TOKEN_RE = re.compile(
|
|
7887
|
+
r"(?<![A-Za-z-])(?:release-handoff\(stage-group\)|release-handoff|done|"
|
|
7888
|
+
r"implementation|error-analysis|implementation-option-selection|"
|
|
7889
|
+
r"implementation-planning)(?![A-Za-z-])"
|
|
7890
|
+
)
|
|
7891
|
+
|
|
7892
|
+
|
|
7806
7893
|
def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
|
|
7807
7894
|
"""Enforce verdict ↔ blocker/condition/routing consistency on the
|
|
7808
7895
|
final-verification data.json (SSOT). The schema guarantees field SHAPE;
|
|
@@ -7817,7 +7904,16 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
|
|
|
7817
7904
|
fv = data.get("finalVerification") or {}
|
|
7818
7905
|
blockers = fv.get("acceptanceBlockers") or []
|
|
7819
7906
|
conditions = verdict.get("conditionalAcceptanceConditions") or []
|
|
7820
|
-
|
|
7907
|
+
routing_value = fv.get("routingRecommendation")
|
|
7908
|
+
routing = routing_value if isinstance(routing_value, str) else ""
|
|
7909
|
+
routing_tokens = _FINAL_VERIFICATION_ROUTING_TOKEN_RE.findall(routing)
|
|
7910
|
+
routing_token = routing_tokens[0] if len(routing_tokens) == 1 else None
|
|
7911
|
+
|
|
7912
|
+
if routing_token is None:
|
|
7913
|
+
failures.append(
|
|
7914
|
+
"final-verification: routingRecommendation must contain exactly one "
|
|
7915
|
+
"supported routing token."
|
|
7916
|
+
)
|
|
7821
7917
|
|
|
7822
7918
|
if token == "accepted" and blockers:
|
|
7823
7919
|
failures.append(
|
|
@@ -7834,7 +7930,10 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
|
|
|
7834
7930
|
"final-verification: verdict `conditional-accept` but "
|
|
7835
7931
|
"conditionalAcceptanceConditions is empty — list every condition."
|
|
7836
7932
|
)
|
|
7837
|
-
if
|
|
7933
|
+
if (
|
|
7934
|
+
routing_token in {"release-handoff", "release-handoff(stage-group)"}
|
|
7935
|
+
and token != "accepted"
|
|
7936
|
+
):
|
|
7838
7937
|
failures.append(
|
|
7839
7938
|
f"final-verification: routingRecommendation cites `release-handoff` "
|
|
7840
7939
|
f"but verdict is `{token}` — release-handoff routing is allowed only "
|
|
@@ -7847,8 +7946,7 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
|
|
|
7847
7946
|
f"final-verification: verificationScope must be `whole-task` or "
|
|
7848
7947
|
f"`single-stage`, got {scope!r}."
|
|
7849
7948
|
)
|
|
7850
|
-
if
|
|
7851
|
-
and "release-handoff(stage-group)" not in routing):
|
|
7949
|
+
if scope == "single-stage" and routing_token == "release-handoff":
|
|
7852
7950
|
failures.append(
|
|
7853
7951
|
"final-verification: verificationScope `single-stage` cannot recommend "
|
|
7854
7952
|
"plain release-handoff routing — a single-stage accepted verdict may "
|
|
@@ -9144,16 +9242,28 @@ def main() -> int:
|
|
|
9144
9242
|
for warning in conformance_warnings:
|
|
9145
9243
|
print(f"validate-run: warning: {warning}", file=sys.stderr)
|
|
9146
9244
|
if task_type in _BRIEF_DERIVED_PHASES:
|
|
9147
|
-
|
|
9245
|
+
planning = validation_data.get("implementationPlanning")
|
|
9246
|
+
selected_direction_plan = (
|
|
9247
|
+
task_type == "implementation-planning"
|
|
9248
|
+
and isinstance(planning, Mapping)
|
|
9249
|
+
and planning.get("planningContract") == "selected-direction"
|
|
9250
|
+
)
|
|
9251
|
+
brief_path = (
|
|
9252
|
+
project_root / "__selected-direction-brief-validated-from-run-manifest__"
|
|
9253
|
+
if selected_direction_plan
|
|
9254
|
+
else _brief_path_from_manifest(task_manifest, project_root)
|
|
9255
|
+
)
|
|
9148
9256
|
if task_type in _END_STATE_PHASES:
|
|
9149
9257
|
if task_type == "implementation-planning":
|
|
9150
9258
|
_validate_planning_conformance_declared(report_path, failures)
|
|
9151
|
-
|
|
9152
|
-
|
|
9259
|
+
if not selected_direction_plan:
|
|
9260
|
+
_validate_end_state_coverage(validation_data, brief_path, failures)
|
|
9261
|
+
if task_type == "implementation-planning" and not selected_direction_plan:
|
|
9153
9262
|
_validate_requirement_provenance(
|
|
9154
9263
|
validation_data, brief_path, failures
|
|
9155
9264
|
)
|
|
9156
9265
|
_validate_stage_has_requirement(validation_data, failures)
|
|
9266
|
+
if task_type == "implementation-planning":
|
|
9157
9267
|
_append_stage_data_failures(validation_data, failures)
|
|
9158
9268
|
if task_type == "improvement-discovery":
|
|
9159
9269
|
run_dir = report_path.parent.parent
|
|
@@ -34,7 +34,11 @@ from okstra_ctl.md_table import split_pipe_row
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
_VERDICT_TOKENS = ("candidates-ready", "no-candidates", "blocked")
|
|
37
|
-
_NEXT_PHASES = (
|
|
37
|
+
_NEXT_PHASES = (
|
|
38
|
+
"requirements-discovery",
|
|
39
|
+
"implementation-option-selection",
|
|
40
|
+
"error-analysis",
|
|
41
|
+
)
|
|
38
42
|
_CAND_ID_RE = re.compile(r"^I-\d{3}$")
|
|
39
43
|
_SOURCE_WORKER_RE = re.compile(r"^([a-z-]+):([A-Za-z0-9._-]+)$")
|
|
40
44
|
_CONSENSUS_VALUES = ("full", "partial", "contested", "worker-unique")
|
|
@@ -16,6 +16,7 @@ Usage:
|
|
|
16
16
|
[--antigravity-model <m>] [--report-writer-model <m>] \\
|
|
17
17
|
[--related-tasks <list>] [--base-ref <ref>] \\
|
|
18
18
|
[--clarification-response <path>] [--work-category <cat>] \\
|
|
19
|
+
[--selected-direction <selection-final-report.md>] \\
|
|
19
20
|
[--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
|
|
20
21
|
[--fix-cycle <yes|no>]
|
|
21
22
|
|
|
@@ -21,6 +21,7 @@ Usage:
|
|
|
21
21
|
[--lead-runtime auto|<host-id-or-alias>] \\
|
|
22
22
|
[--related-tasks <list>] [--base-ref <ref>] \\
|
|
23
23
|
[--clarification-response <path>] [--work-category <cat>] \\
|
|
24
|
+
[--selected-direction <selection-final-report.md>] \\
|
|
24
25
|
[--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
|
|
25
26
|
[--fix-cycle <yes|no>]
|
|
26
27
|
|