okstra 0.219.1 → 0.220.1
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/dist/cli-registry.mjs +18 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/lib/paths.mjs +1 -1
- package/dist/lib/paths.mjs.map +1 -1
- package/docs/cli.md +4 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -4
- package/runtime/python/okstra_ctl/blocking_checks.py +8 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +10 -2
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -2
- package/runtime/python/okstra_ctl/correction_cli.py +59 -0
- package/runtime/python/okstra_ctl/correction_round.py +168 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +18 -0
- package/runtime/python/okstra_ctl/lead_progress.py +56 -11
- package/runtime/python/okstra_ctl/manager_cli.py +2 -0
- package/runtime/python/okstra_ctl/manager_launch.py +23 -2
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +29 -6
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -0
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +6 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +2 -1
- package/runtime/python/okstra_ctl/phases/implementation/validation.py +62 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +2 -2
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +2 -2
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/report_assembly.py +5 -5
- package/runtime/python/okstra_ctl/report_inputs.py +10 -1
- package/runtime/python/okstra_ctl/stage_done.py +183 -0
- package/runtime/python/okstra_ctl/stage_fail.py +114 -0
- package/runtime/python/okstra_ctl/stage_reopen.py +12 -8
- package/runtime/python/okstra_ctl/wizard/engine.py +1 -2
- package/runtime/schemas/final-report-v3.0.schema.json +78 -0
- package/runtime/skills/okstra-manager/SKILL.md +2 -2
- package/runtime/templates/reports/html/i18n/en.json +6 -0
- package/runtime/templates/reports/html/i18n/ko.json +6 -0
- package/runtime/validators/validate-run.py +2 -0
|
@@ -18,12 +18,14 @@ import argparse
|
|
|
18
18
|
import json
|
|
19
19
|
import sys
|
|
20
20
|
from collections.abc import Mapping, Sequence
|
|
21
|
+
from dataclasses import replace
|
|
21
22
|
from datetime import datetime, timezone
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import Any
|
|
24
25
|
|
|
25
26
|
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
26
|
-
from okstra_ctl.lead_events import LeadEvent, append_lead_event
|
|
27
|
+
from okstra_ctl.lead_events import LeadEvent, append_lead_event, read_lead_events
|
|
28
|
+
from okstra_ctl.phases import catalog
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class ProgressRecordError(ValueError):
|
|
@@ -64,6 +66,11 @@ _DEFAULT_DETAIL: Mapping[str, str] = {
|
|
|
64
66
|
"phase-7-teardown": "shutting-down-workers",
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
# phase 하나에 문구가 둘인 체크포인트. 고정 표만으로는 두 번째 호출이 첫 문구를
|
|
70
|
+
# 다시 내므로, 원장과 task type 을 보고 고른다.
|
|
71
|
+
_INTAKE_COMPLETE_DETAIL = "complete"
|
|
72
|
+
_LEAD_ONLY_SYNTHESIS_DETAIL = "authoring final report"
|
|
73
|
+
|
|
67
74
|
|
|
68
75
|
def _utc_now() -> str:
|
|
69
76
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -165,6 +172,39 @@ def _parse_field(raw: str) -> tuple[str, str]:
|
|
|
165
172
|
return name.strip(), field_value.strip()
|
|
166
173
|
|
|
167
174
|
|
|
175
|
+
def _intake_already_started(events_path: Path, event: LeadEvent) -> bool:
|
|
176
|
+
return any(
|
|
177
|
+
prior.event_type == "progress"
|
|
178
|
+
and prior.details.get("phase") == "phase-1-intake"
|
|
179
|
+
and (prior.task_key, prior.task_type, prior.run_seq)
|
|
180
|
+
== (event.task_key, event.task_type, event.run_seq)
|
|
181
|
+
for prior in read_lead_events(events_path)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _task_type_dispatches_report_writer(task_type: str) -> bool:
|
|
186
|
+
if task_type not in catalog.task_types():
|
|
187
|
+
return True
|
|
188
|
+
package = catalog.phase_record(task_type).package_name
|
|
189
|
+
profile = _read_json_object(
|
|
190
|
+
Path(catalog.__file__).parent / package / "profile.json", "phase profile"
|
|
191
|
+
)
|
|
192
|
+
return any(
|
|
193
|
+
isinstance(row, Mapping) and row.get("roleId") == "report-writer"
|
|
194
|
+
for row in profile.get("roles") or []
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _default_detail(phase: str, events_path: Path, event: LeadEvent) -> str | None:
|
|
199
|
+
if phase == "phase-1-intake" and _intake_already_started(events_path, event):
|
|
200
|
+
return _INTAKE_COMPLETE_DETAIL
|
|
201
|
+
if phase == "phase-6-synthesis" and not _task_type_dispatches_report_writer(
|
|
202
|
+
event.task_type
|
|
203
|
+
):
|
|
204
|
+
return _LEAD_ONLY_SYNTHESIS_DETAIL
|
|
205
|
+
return _DEFAULT_DETAIL.get(phase)
|
|
206
|
+
|
|
207
|
+
|
|
168
208
|
def render_progress_line(
|
|
169
209
|
phase: str,
|
|
170
210
|
fields: Sequence[tuple[str, str]] = (),
|
|
@@ -199,7 +239,9 @@ def record_progress(
|
|
|
199
239
|
)
|
|
200
240
|
ordered.append(("worker", role))
|
|
201
241
|
ordered.extend(fields)
|
|
202
|
-
|
|
242
|
+
events_path = _resolve_project_path(
|
|
243
|
+
project_root, _require_string(manifest, "leadEventsPath")
|
|
244
|
+
)
|
|
203
245
|
event = LeadEvent(
|
|
204
246
|
event_type="progress",
|
|
205
247
|
lead_runtime=_require_string(manifest, "leadRuntime"),
|
|
@@ -207,15 +249,15 @@ def record_progress(
|
|
|
207
249
|
task_type=_require_string(manifest, "taskType"),
|
|
208
250
|
run_seq=_manifest_run_seq(manifest),
|
|
209
251
|
timestamp=timestamp or _utc_now(),
|
|
210
|
-
details={
|
|
211
|
-
"phase": phase,
|
|
212
|
-
"line": line,
|
|
213
|
-
**({name: field_value for name, field_value in ordered}),
|
|
214
|
-
},
|
|
215
|
-
)
|
|
216
|
-
events_path = _resolve_project_path(
|
|
217
|
-
project_root, _require_string(manifest, "leadEventsPath")
|
|
218
252
|
)
|
|
253
|
+
if detail is None:
|
|
254
|
+
detail = _default_detail(phase, events_path, event)
|
|
255
|
+
line = render_progress_line(phase, ordered, detail)
|
|
256
|
+
event = replace(event, details={
|
|
257
|
+
"phase": phase,
|
|
258
|
+
"line": line,
|
|
259
|
+
**({name: field_value for name, field_value in ordered}),
|
|
260
|
+
})
|
|
219
261
|
append_lead_event(events_path, event)
|
|
220
262
|
return event, warning
|
|
221
263
|
|
|
@@ -306,7 +348,10 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
306
348
|
help="extra `NAME=VALUE` token, in the order given")
|
|
307
349
|
append_parser.add_argument(
|
|
308
350
|
"--detail",
|
|
309
|
-
help="verb phrase after the fields; contract wording is the default"
|
|
351
|
+
help="verb phrase after the fields; contract wording is the default "
|
|
352
|
+
"(a second phase-1-intake renders `complete`, and phase-6-synthesis "
|
|
353
|
+
"in a task type without a report writer renders "
|
|
354
|
+
"`authoring final report`)")
|
|
310
355
|
return parser
|
|
311
356
|
|
|
312
357
|
|
|
@@ -332,6 +332,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
332
332
|
run.add_argument("--task-group", required=True)
|
|
333
333
|
run.add_argument("--task-id", required=True)
|
|
334
334
|
run.add_argument("--child-task-id", default=None)
|
|
335
|
+
run.add_argument("--allow-completed", action="store_true")
|
|
335
336
|
|
|
336
337
|
split = task_sub.add_parser("split")
|
|
337
338
|
split.add_argument("--manager-id", required=True)
|
|
@@ -466,6 +467,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
466
467
|
args.task_id,
|
|
467
468
|
args.project_id,
|
|
468
469
|
child_task_id=args.child_task_id,
|
|
470
|
+
allow_completed=args.allow_completed,
|
|
469
471
|
),
|
|
470
472
|
args.json,
|
|
471
473
|
)
|
|
@@ -20,7 +20,7 @@ from .manager_paths import (
|
|
|
20
20
|
task_manifest_path,
|
|
21
21
|
)
|
|
22
22
|
from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default, _write_json
|
|
23
|
-
from .manager_sync import
|
|
23
|
+
from .manager_sync import _snapshot_child, summary_bucket
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
WORKER_DISPATCH_BACKEND = "subagent"
|
|
@@ -222,6 +222,25 @@ def _record_launch_prepared(
|
|
|
222
222
|
)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def _current_child_state(project_root: str, child: dict, target_task_key: str, *, allow_completed: bool) -> dict:
|
|
226
|
+
"""대상 하위 task 를 마지막 sync 가 아니라 지금 프로젝트에서 다시 읽는다.
|
|
227
|
+
|
|
228
|
+
스냅샷은 sync 시점 값이라, 그 뒤 프로젝트에서 끝난 task 에 런치 패킷을 만들면 같은 일을 두 번 시킨다.
|
|
229
|
+
"""
|
|
230
|
+
current = _snapshot_child(Path(project_root), {**child, "taskKey": target_task_key})
|
|
231
|
+
if current.get("error"):
|
|
232
|
+
raise ManagerError(f"{target_task_key}: cannot read child task state: {current['error']}")
|
|
233
|
+
if summary_bucket(current) == "done" and not allow_completed:
|
|
234
|
+
report = current.get("latestReportRecordPath") or ""
|
|
235
|
+
raise ManagerError(
|
|
236
|
+
f"{target_task_key}: already done in its project "
|
|
237
|
+
f"(last phase {current.get('lastCompletedPhase') or current.get('currentPhase') or 'unknown'}, "
|
|
238
|
+
f"report {Path(project_root) / report if report else 'none'}); nothing to launch. "
|
|
239
|
+
"Run `okstra manager task sync` to refresh the manager, or pass --allow-completed to prepare a launch anyway"
|
|
240
|
+
)
|
|
241
|
+
return current
|
|
242
|
+
|
|
243
|
+
|
|
225
244
|
def build_launch_packet(
|
|
226
245
|
home: Path,
|
|
227
246
|
manager_id: str,
|
|
@@ -230,6 +249,7 @@ def build_launch_packet(
|
|
|
230
249
|
project_id: str,
|
|
231
250
|
*,
|
|
232
251
|
child_task_id: str | None = None,
|
|
252
|
+
allow_completed: bool = False,
|
|
233
253
|
now: str | None = None,
|
|
234
254
|
) -> dict:
|
|
235
255
|
manifest = _read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
@@ -244,6 +264,7 @@ def build_launch_packet(
|
|
|
244
264
|
target_task_key = _target_task_key(project_id, task_group, child_task_id_value)
|
|
245
265
|
_validate_child_task_key(child, target_task_key)
|
|
246
266
|
project_root = _validated_project_root(project)
|
|
267
|
+
current = _current_child_state(project_root, child, target_task_key, allow_completed=allow_completed)
|
|
247
268
|
created_at = now or _now_iso()
|
|
248
269
|
brief_path = (Path(str(child["briefPath"])) if child.get("briefPath")
|
|
249
270
|
else _discussion_follow_up_brief(snapshot, target_task_key))
|
|
@@ -277,7 +298,7 @@ def build_launch_packet(
|
|
|
277
298
|
# phase 를 채워야 하는데, 두 값이 다 주어지면 그 채우기를 건너뛴다(autofill_from_manifest).
|
|
278
299
|
# 토론 후속 브리프는 권장 phase 를 따로 적지 않아도 requirements-discovery 에서 시작한다.
|
|
279
300
|
recommended = child.get("recommendedPhase") or (DISCUSSION_FOLLOW_UP_PHASE if discussion_ref else "")
|
|
280
|
-
if recommended and
|
|
301
|
+
if recommended and not current["exists"]:
|
|
281
302
|
run_args.extend(["--task-type", str(recommended)])
|
|
282
303
|
if brief_path is not None:
|
|
283
304
|
run_args.extend(["--task-brief", str(brief_path)])
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md
CHANGED
|
@@ -13,11 +13,12 @@ are collected and convergence finished. Phase 1-5 do not need it.
|
|
|
13
13
|
- **Plan link & approval evidence**: path to the approved `final-report.md`, the exact quoted approval marker, AND the executed stage number / title quoted from the Stage Map row. For a selected-direction plan, also quote `selectedDirectionRef.optionId`, `snapshotPath`, and the validated snapshot digest; for a legacy plan, quote the effective `implementation-option` or the Recommended Option fallback.
|
|
14
14
|
- **Commit list**: each commit's SHA (or short SHA), message, and the plan step(s) / TDD cycle it satisfies. A lead minor fix commit is listed too, with `planStep` `Lead minor fix (<finding ids>)`.
|
|
15
15
|
- **Lead minor fixes**: when the lead fixed verifier findings in this run, pass each `implementation.leadFixes[]` entry to the report writer as recorded (§"Lead minor fix"). Omit the field when there was none.
|
|
16
|
+
- **Correction rounds**: when this run ran correction rounds, pass one `implementation.correctionRounds[]` entry per round to the report writer — `round`, `findingIds`, `baseCommit`, `commit`, each verifier's re-verification `verifierVerdicts`, and `result` (§"Correction round"). `verifierResults` then carries the last round's verdicts. Omit the field when there was none.
|
|
16
17
|
- **Diff summary**: capture `git diff --stat <base>..<captured-head>` and `git diff --numstat <base>..<captured-head>` for the full implementation stage, including after a fix run. Supply both outputs to the report writer; put each file's added/deleted counts alone in `diffSummary.files[].lines` (`+150/-0`) and one sentence on what the change in that file does in `diffSummary.files[].summary` — the report renders `summary` as the file's "what it does" column and `lines` as a separate number column. Do not substitute a fix-run aggregate or carry forward older statistics. If the captured commits are unavailable, identify the missing reference in the evidence instead of inventing counts.
|
|
17
18
|
- **Change narrative scope**: `userNarrative.changeExplanation` describes the same scope as `diffSummary` — the whole stage from its base, every file the table lists. On a fix run, state what the fix run itself changed (its commits and files) as a separate sentence; never present the fix-run delta as the stage's change.
|
|
18
19
|
- **Out-of-plan edits block**: every file edited that was not in the approved plan's file list, with rationale (empty block is acceptable and preferred)
|
|
19
20
|
- **Clarification answers carried in**: when `instruction-set/clarification-response.md` exists, it holds the user's answers to the approved plan's `## 1. Clarification Items` rows. The report MUST render the `## 0. Clarification Response Carried In From Previous Run` section (set `clarificationCarryIn.sourceFile` to that path) and, for every carried-over `## 1.` row whose answer appears there, record the answer in the `User input` cell and flip `Status` to `resolved`. A row left `open`/`answered` despite a matching user answer is a contract violation; an answer that materially changes scope beyond the approved plan is routed to a new `implementation-planning` run, not silently executed.
|
|
20
|
-
- **Stage sidecar evidence**: set `stageSidecarEvidence.carryJson` to the
|
|
21
|
+
- **Stage sidecar evidence**: set `stageSidecarEvidence.carryJson` to the `carryPath` that `okstra stage-done` printed — the flat `runs/implementation/carry/stage-<N>.json`, never a path under the run's `stage-<N>/` directory — followed by a one-line summary of its payload — stage number, step results by status, stage commit range, number of files changed. Do not paste the payload itself: the file on disk is the record (`_validate_stage_carry_sidecar_exists` checks that it exists), and a pasted one-line JSON is unreadable in the report. Quote the `consumers.jsonl` rows this run appended line-by-line in `consumerRows`.
|
|
21
22
|
- **Validation evidence**: actual command output (stdout/stderr) for every `pre / mid / post` validation command from the plan. Truncated output is acceptable but the command line and exit code MUST be exact. No paraphrasing of test results. Set `checkId` to the plan's `validationChecklist` id (`VC-007`) when the row runs a checklist command, and `advisory: true` on a row whose check is advisory (an external Tier 3 conformance entry, for example) so a non-zero exit there is not shown as a failure.
|
|
22
23
|
- **TDD evidence (when applicable)**: for steps that should be TDD-ordered, show the failing-test output captured BEFORE the merged commit and the passing-test output after, citing the single `feat|fix` commit SHA (test and implementation share one commit).
|
|
23
24
|
- **Verifier results**: a section per verifier present in the resolved roster (`Claude verifier`, `Codex verifier`, and `Antigravity verifier` when opted in) containing:
|
|
@@ -46,7 +47,7 @@ are collected and convergence finished. Phase 1-5 do not need it.
|
|
|
46
47
|
|
|
47
48
|
When the synthesised verdict would be `FAIL` only because of verifier findings that are small and local, the lead fixes them itself in this run instead of ending it and opening a fix run. Decide after Phase 5.5 convergence, before building the report-writer dispatch prompt and before "Lead post-stage persistence" below. No worker dispatch is open then, so the edit falls inside no read-only worker's mutation audit window. Never edit while a verifier, convergence, or report-writer dispatch is running: its audit reports `readonly source changed` and discards that worker's result.
|
|
48
49
|
|
|
49
|
-
**Eligibility.** Every rule holds for every blocking finding of the round, or the lead fixes none of them and the
|
|
50
|
+
**Eligibility.** Every rule holds for every blocking finding of the round, or the lead fixes none of them and §"Correction round" decides whether the executor fixes them in this run:
|
|
50
51
|
|
|
51
52
|
1. The fix is a code change at, or next to, the location the verifier cited.
|
|
52
53
|
2. The lead's total diff is at most 30 changed lines (added plus deleted, `git diff --numstat`).
|
|
@@ -65,10 +66,32 @@ A finding that needs no code change (convergence settled it as a verifier misrea
|
|
|
65
66
|
|
|
66
67
|
**Record.** One `implementation.leadFixes[]` entry per fixed finding group: `findingIds`, `verifiers` (the `verifierResults[].verifier` names whose FAIL it answers — every `FAIL` row is named by a passing entry before the stage may settle), `summary` (one sentence on what changed), `commit` (full SHA), `files`, `changedLines`, `commands[]` (`command`, `exitCode`, `outputTail`), and `result`. Keep each verifier's `FAIL` row as written. Leave the fixed findings out of `humanSummary.blockers`, and state in `userNarrative.validationExplanation` that the lead fixed them in this run.
|
|
67
68
|
|
|
68
|
-
**Persistence after a passing fix.** The executor's `### Stage Carry Evidence` was emitted before the fix and names the pre-fix head. Before
|
|
69
|
+
**Persistence after a passing fix.** The executor's `### Stage Carry Evidence` was emitted before the fix and names the pre-fix head. Before passing the carry to `okstra stage-done`, set `stageCommitRange.head` to the post-fix HEAD (`git rev-parse HEAD` in the stage worktree), add the fixed files to `filesChanged`, and append one `notes` line naming the lead commit and the finding ids. `stepResults` stays as the executor wrote it: a lead fix is not a plan step. Append the `done` row after the fix commit, so its `head_commit` is the post-fix HEAD, and capture `diffSummary` against the same head.
|
|
69
70
|
|
|
70
71
|
**Enforced:** `validators/validate-run.py` `_validate_lead_fixes` (blocking) fails an entry recorded `pass` whose command exited non-zero or that has no command, an entry over 30 changed lines, and a settled stage holding a `result: fail` entry; it reports (advisory) a settled stage whose `FAIL` row no passing entry names in `verifiers` — advisory because validate-run runs after the `done` row is appended, so blocking would undo nothing. `_validate_lead_fix_carry_head` (advisory) reports a carry sidecar whose head is not a passing lead fix commit. Eligibility rules 1, 3, 4 and 5 and the match between findings and `findingIds` are instructions only: the report carries no structured finding id or planned-path list to compare against.
|
|
71
72
|
|
|
73
|
+
## Correction round (decided after convergence, before the report-writer dispatch)
|
|
74
|
+
|
|
75
|
+
When the synthesised verdict is `FAIL` and the lead minor fix does not apply, the executor fixes the confirmed findings in this run and the verifiers re-verify the fix, instead of ending the run and opening a fix run. At most **2** rounds per run.
|
|
76
|
+
|
|
77
|
+
**Entry.** Every blocking finding of the round meets all of these, or the run ends `FAIL` as before:
|
|
78
|
+
|
|
79
|
+
1. A verifier cited where it is (`path:line`) and what the correct result is.
|
|
80
|
+
2. The fix stays inside this stage's `plannedPaths` or files the executor already changed.
|
|
81
|
+
3. It needs no new plan step, dependency, schema, migration, or config of record — a finding that does is a plan change: end the run `FAIL` and route by `prompts/lead/phase-routing.md`.
|
|
82
|
+
|
|
83
|
+
Changing a test's expected value is allowed here when the finding shows the oracle is wrong, because the verifiers re-verify the fix.
|
|
84
|
+
|
|
85
|
+
**Procedure for round `<N>`.**
|
|
86
|
+
|
|
87
|
+
1. Record the stage HEAD as `<base>` (`git rev-parse HEAD` in the stage worktree).
|
|
88
|
+
2. Write the executor instruction with `okstra correction prompt --run-manifest <path> --round <N> --role executor --base <base> --finding <F-id> [--finding <F-id> ...]` into a file under the run's `state/`, then materialize it with `okstra agent-prompt materialize` — `--audience implementation-executor`, `--assignment-ref initial/<executor worker id>`, `--dispatch-kind correction-r<N>`, a new `--invocation-id` ending `-correction-r<N>-<executor worker id>`, and a result path `<executor worker id>-worker-correction-r<N>-implementation-<seq>.md`. Build a jobs file with `okstra agent-prompt jobs --dispatch-kind correction-r<N>`, dispatch it alone with `okstra team dispatch --jobs-file <path> --dispatch-kind correction-r<N>`, and settle it with `okstra team await`.
|
|
89
|
+
3. Do the same for every verifier with `--role verifier`, `--audience implementation-verifier`, its own `initial/<verifier worker id>` assignment, and result `<verifier worker id>-worker-correction-r<N>-implementation-<seq>.md`; dispatch the verifiers in one batch without the executor.
|
|
90
|
+
4. Converge the re-verification results the way the initial round was converged, restarting Phase 5.5 with `okstra convergence seed ... --restart-from-round0`.
|
|
91
|
+
5. No re-verification verdict is `FAIL`: the round `result` is `pass` and the run continues to persistence with the post-fix head. Otherwise `result: fail`; start round `<N+1>` if the cap allows and the entry rules still hold, or end the run `FAIL`.
|
|
92
|
+
|
|
93
|
+
**Enforced:** `scripts/okstra_ctl/dispatch_core.py` `_validate_correction_rounds` (via `correction_round.validate_correction_dispatch`) refuses a round past the cap, a verifier dispatch before that round's executor returned an `ok` result, a second executor fix in the same round, and round `<N+1>` before round `<N>` was re-verified. `validators/validate-run.py` `_validate_correction_rounds` (blocking) fails a report whose `correctionRounds` differ from the correction rounds the run dispatched, whose rounds are not 1, 2 in order, whose `result` contradicts its verdicts, or whose `verifierResults` are not the last round's verdicts. The entry rules are instructions only.
|
|
94
|
+
|
|
72
95
|
## Self-review pass before finalising the report (the Okstra lead runs this; do not delegate it)
|
|
73
96
|
|
|
74
97
|
1. **Plan coverage** — for a selected-direction plan, every step in the approved `plan-ready` stage must point to a commit (or an explicit `Skipped: <reason>` entry), and the diff must preserve the selected snapshot's mechanism and invariants. For a legacy plan, every step in the effective implementation option (explicit frontmatter value or Recommended Option fallback) must point to a commit or an explicit skip. List gaps. A `RED:` step and its `GREEN:` step pointing to the same merged commit SHA is NOT a coverage gap — one SHA may be shared by both.
|
|
@@ -90,10 +113,10 @@ A finding that needs no code change (convergence settled it as a verifier misrea
|
|
|
90
113
|
|
|
91
114
|
- Parse the executor's `### Stage Carry Evidence` JSON block. If absent or unparsable, end with status `contract-violated` and route to a follow-up `error-analysis`.
|
|
92
115
|
- The `### Stage Carry Evidence` JSON may include `designPrepEvidence[]`. Emit a row only when this stage produced concrete evidence that refines an effective PREP item: `itemId`, the injected `assessmentFingerprint`, `resolution`, and non-empty `evidence[]` are required; `overrides` is optional and only records observed, non-authoritative refinements. Carry evidence never represents user approval. Downstream resolution accepts it only from transitive dependency stages with the matching fingerprint.
|
|
93
|
-
- **A `FAIL` synthesised verdict withholds the two writes below.** A verifier `FAIL` that a passing lead minor fix answered does not make the verdict `FAIL` (§"Lead minor fix"); persist with the post-fix head it names. They are what marks the stage `done`, so performing them on a stage whose verifier found a blocking defect stacks the next stage on a confirmed regression. When the synthesised verdict is `FAIL`: write NO carry sidecar, and append a `status:"failed"` row in place of the `done` row
|
|
116
|
+
- **A `FAIL` synthesised verdict withholds the two writes below.** A verifier `FAIL` that a passing lead minor fix answered does not make the verdict `FAIL` (§"Lead minor fix"); persist with the post-fix head it names. They are what marks the stage `done`, so performing them on a stage whose verifier found a blocking defect stacks the next stage on a confirmed regression. When the synthesised verdict is `FAIL`: write NO carry sidecar, and append a `status:"failed"` row in place of the `done` row with `okstra stage-fail <impl-task-key> --stage <N> --head-commit <HEAD sha> --report <this run's final-report path> --reason "<blocking finding ids>"` (it calls `append_consumer`, so the consumers lock and the registry release apply; do not call python directly). That row is terminal *without* completion: dependent stages stay blocked because this stage is not done, while its worktree-registry occupancy is released so a fix run can re-enter the same stage number — `--stage <N>` reuses the preserved worktree and branch instead of provisioning a new one. State the reason in the report's `Stage sidecar evidence` section as `withheld`. **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` accepts a missing carry file only when that field is non-empty, so silently skipping the sidecar still fails the run.
|
|
94
117
|
- **A run that ends with no verifier result still closes.** When the stage ends before any verifier returned a verdict — the executor stopped without a change and the user chose not to verify, or every dispatched verifier ended without a result — append the `failed` row as above, record each roster verifier `not-run` with its reason (`okstra worker-state transition --team-state <path> --worker <worker-id> --status not-run --reason <text>`; a worker the dispatch skipped already carries one), and write the convergence state with `okstra convergence skip --run-manifest <path> --reason <text>` before dispatching the report writer. Without that state the report-writer packet refuses with `convergence … required source is missing`. The command refuses while any analyser, verifier, or critic attempt returned a result or is still running; such a run converges normally. In the report, write each verifier's `verifierResults[]` row as `verdict: not-run`, and state the reason under `Stage sidecar evidence` as `withheld`.
|
|
95
|
-
- On a non-`FAIL` verdict, for this run's single stage:
|
|
96
|
-
-
|
|
118
|
+
- On a non-`FAIL` verdict, for this run's single stage: save the executor's `### Stage Carry Evidence` JSON (after a passing lead minor fix, with the head, files, and note that §"Lead minor fix" adds; after a correction round, the last round's executor block) to a file under this run's `state/`, then run `okstra stage-done <impl-task-key> --stage <N> --carry <that file> --report <this run's final-report path>`. It writes the sidecar to the flat `runs/implementation/carry/stage-<N>.json` (the readers glob `carry/stage-*.json`: `okstra_ctl.stage_map`, `okstra_ctl.consumers.backfill_done_from_carry`, and validate-run's `RunRef.carry`), refuses to replace another head's sidecar of a stage that was never settled, and for a stage reopened after a `done` row (see "Reopening a settled stage" below) moves the previous sidecar unchanged to `carry/superseded/stage-<N>-from-<task-type>-<seq>.json` first. Name the `carryPath` and any `supersededCarryPath` it prints in `Stage sidecar evidence`. A reopened stage whose run ends `FAIL` writes nothing and leaves the old file where it is.
|
|
119
|
+
- The same `okstra stage-done` call appends the `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (relative to the report's run directory) and the carry's head, through `append_consumer` — so the consumers lock applies and this stage's worktree-registry occupancy is released. Never call `append_consumer` from python. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
|
|
97
120
|
- **Automatic stage worktree cleanup:** after the report passes finalization, `report-finalize` reclaims this completed stage's clean worktree. It compares the report's final commit with the completion ledger and branch tip, preserves active or unfinished stages and dirty or nested worktrees, and keeps the branch and registry base for dependency rebases and later re-entry. Never remove the directory by hand before finalization. A later implementation or final-verification entry restores it from the preserved branch. After finalization, use the main project root or absolute artifact paths: this run's stage cwd may no longer exist. Enforcement: `report_finalize._teardown_stage_worktrees`, `stage_targets.teardown_completed_stage_worktrees`, and the cleanup/restoration tests in `tests/worktree/test_stage_rebase.py`.
|
|
98
121
|
- **Reopening a settled stage.** Appending a `status:"failed"` row after a `done` one withdraws that stage: `--stage N` accepts it again, its dependents stop resolving a base from the withdrawn head, and whole-task final-verification blocks until it is settled again. Write it with `okstra stage-reopen <task-key> --stage <N> --reason "<why>" [--evidence <report>]`, which records the reason and the report that found the defect, and re-run the stage rather than repairing the tree outside okstra — a stage reverted outside the ledger leaves the recorded `head_commit` and the carry sidecar naming a tree that no longer exists, and the next stage branches from it.
|
|
99
122
|
- The verifier round, Phase 5.5 convergence, and this Phase 6 report run **once per run** over this stage's diff — NOT per step.
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md
CHANGED
|
@@ -220,6 +220,8 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
|
|
|
220
220
|
|
|
221
221
|
### Fix-run incremental scope (applies when the profile carries a "Fix-Run Carry" block)
|
|
222
222
|
|
|
223
|
+
A correction-round re-verification (`--dispatch-kind correction-r<N>`, a "Correction Round" block in the task instructions) takes the same scope: the block's findings and the correction diff it names, with every validation command re-run.
|
|
224
|
+
|
|
223
225
|
A fix run re-verifies a stage whose previous run already passed a full static
|
|
224
226
|
sweep and failed only on cited blocking findings. Re-sweeping the whole stage
|
|
225
227
|
diff re-buys wall-clock without new information, so the static scope narrows —
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Implementation Profile
|
|
2
2
|
|
|
3
3
|
- Purpose: realise the approved `implementation-planning` deliverable as actual source changes, with cross-model verification, while keeping the run reversible
|
|
4
|
-
- **Run-level fixed cost:** the verifier set
|
|
4
|
+
- **Run-level fixed cost:** the verifier set and Phase 5.5 convergence run once per implementation run over this run's single stage diff — never once per step — plus once per correction round; the Phase 6 report-writer runs exactly once.
|
|
5
5
|
- **Fix run (profile carries a "Fix-Run Carry" block):** the executor's scope is the carried blocking findings plus the previous routing recommendation — it MUST NOT re-execute plan steps the previous run completed. Verifiers apply the "Fix-run incremental scope" section of `_implementation-verifier.md`. The full validation-command re-run is NOT reduced.
|
|
6
6
|
- **Lead minor fix:** a verifier FAIL whose findings are small and local is fixed by the lead inside this run, after convergence and before the report-writer dispatch, under `_implementation-deliverable.md` "Lead minor fix"; a passing fix settles the stage without a fix run.
|
|
7
|
+
- **Correction round:** a verifier FAIL whose findings are confirmed, located, and inside the stage's plan is fixed by the executor and re-verified inside this run, at most 2 rounds, under `_implementation-deliverable.md` "Correction round"; a finding that needs a plan change still ends the run `FAIL`.
|
|
7
8
|
- **Executor binding (resolved at run-prep time, fixed for this run):**
|
|
8
9
|
- Executor display name: `{{EXECUTOR_DISPLAY_NAME}}`
|
|
9
10
|
- Executor worker ID: `{{EXECUTOR_WORKER_ID}}`
|
package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html
CHANGED
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
{% for row in implementation.leadFixes %}<article class="summary-card tone-{{ 'neutral' if row.result == 'pass' else 'important' }}"><p class="eyebrow">{{ row.findingIds | join(', ') | inline_code }} · <span class="status status-{{ row.result }}">{{ t('tasks.implementation.lead-fix-result-' ~ row.result) }}</span></p><p>{{ row.summary | inline_code }}</p><table><tbody><tr><th>{{ t('tasks.implementation.commit') }}</th><td><code>{{ row.commit }}</code></td></tr><tr><th>{{ t('tasks.implementation.lead-fix-verifiers') }}</th><td>{{ row.verifiers | join(', ') | inline_code }}</td></tr><tr><th>{{ t('tasks.implementation.files') }}</th><td>{% for path in row.files %}<code>{{ path }}</code>{% if not loop.last %}, {% endif %}{% endfor %}</td></tr><tr><th>{{ t('tasks.implementation.lead-fix-changed-lines') }}</th><td class="figure">{{ row.changedLines }}</td></tr></tbody></table>{% for command in row.commands %}<details><summary><code title="{{ command.command }}">{{ command.command }}</code> · {{ t('tasks.implementation.exit-code') }} {{ command.exitCode }}</summary><pre>{{ command.outputTail }}</pre></details>{% endfor %}</article>{% endfor %}
|
|
33
33
|
</section>{% endif %}
|
|
34
34
|
|
|
35
|
+
{% if implementation.get("correctionRounds") %}<section data-report-section="correction-rounds" data-report-field="implementation.correctionRounds">
|
|
36
|
+
<h2>{{ t('tasks.implementation.correction-rounds') }}</h2>
|
|
37
|
+
<p>{{ t('tasks.implementation.correction-rounds-intro') }}</p>
|
|
38
|
+
{% for row in implementation.correctionRounds %}<article class="summary-card tone-{{ 'neutral' if row.result == 'pass' else 'important' }}"><p class="eyebrow">{{ t('tasks.implementation.correction-round') }} {{ row.round }} · {{ row.findingIds | join(', ') | inline_code }} · <span class="status status-{{ row.result }}">{{ t('tasks.implementation.correction-result-' ~ row.result) }}</span></p><table><tbody><tr><th>{{ t('tasks.implementation.correction-range') }}</th><td><code>{{ row.baseCommit }}</code>..<code>{{ row.commit }}</code></td></tr>{% for verdict in row.verifierVerdicts %}<tr><th>{{ verdict.verifier | inline_code }}</th><td><span class="status status-{{ verdict.verdict | lower }}">{{ verdict.verdict }}</span></td></tr>{% endfor %}</tbody></table></article>{% endfor %}
|
|
39
|
+
</section>{% endif %}
|
|
40
|
+
|
|
35
41
|
<section data-report-section="remaining-issues">
|
|
36
42
|
<h2>{{ t('tasks.implementation.what-is-left') }}</h2>
|
|
37
43
|
{{ render_narrative(narrative.remainingIssues, "implementation.userNarrative.remainingIssues") }}
|
|
@@ -148,7 +148,7 @@ flowchart TD
|
|
|
148
148
|
Verdict --> Report[Final report preserves dissent]
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
Only the executor may mutate project files, with one exception: after the verifier round and convergence, the lead may fix small, local verifier FAIL findings itself (at most 30 changed lines, no test-expectation, dependency, schema, migration or config-of-record change), re-run the checks that found them, and settle the stage when they pass, instead of opening a fix run (`instructions/_implementation-deliverable.md` "Lead minor fix"). The verifier independently re-runs the diff and validation command read-only in the same worktree. Even a verifier with the same provider as the executor runs again in a separate fresh CLI session. This is to prevent a structure where the same session approves a diff the same session wrote.
|
|
151
|
+
Only the executor may mutate project files, with one exception: after the verifier round and convergence, the lead may fix small, local verifier FAIL findings itself (at most 30 changed lines, no test-expectation, dependency, schema, migration or config-of-record change), re-run the checks that found them, and settle the stage when they pass, instead of opening a fix run (`instructions/_implementation-deliverable.md` "Lead minor fix"). Findings outside those limits but inside the stage's plan go to a correction round: the executor fixes them under dispatch kind `correction-r<N>` and the verifiers re-verify the fix, at most 2 rounds per run, before the report is written once (`"Correction round"`). The verifier independently re-runs the diff and validation command read-only in the same worktree. Even a verifier with the same provider as the executor runs again in a separate fresh CLI session. This is to prevent a structure where the same session approves a diff the same session wrote.
|
|
152
152
|
|
|
153
153
|
## 5. stage and consumers
|
|
154
154
|
|
|
@@ -200,6 +200,7 @@ The final report requires at least the following.
|
|
|
200
200
|
- TDD failing-then-passing evidence
|
|
201
201
|
- per-verifier independent validation rerun result
|
|
202
202
|
- lead minor fixes, when any: finding ids, commit, files, changed lines, and re-run checks (`implementation.leadFixes`)
|
|
203
|
+
- correction rounds, when any: round, finding ids, fix commit range, each verifier's re-verification verdict, and result (`implementation.correctionRounds`)
|
|
203
204
|
- `carry/stage-<N>.json` evidence sidecar and `consumers.jsonl` started/done row
|
|
204
205
|
- rollback verification
|
|
205
206
|
- `implementation.manualUserTest` finalized against the actual diff and whether it is executable
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
|
-
from collections.abc import Callable
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ...correction_round import correction_round
|
|
6
9
|
|
|
7
10
|
_PASSING_VERDICT_TOKENS = frozenset({"accepted", "conditional-accept"})
|
|
8
11
|
_CHECKLIST_ID_RE = re.compile(r"\bVC-\d{3,}\b")
|
|
@@ -266,6 +269,64 @@ def _validate_lead_fixes(data: dict, failures: list[str]) -> None:
|
|
|
266
269
|
)
|
|
267
270
|
|
|
268
271
|
|
|
272
|
+
_CORRECTION_BLOCKING = "correction-round BLOCKING:"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _validate_correction_rounds(
|
|
276
|
+
data: dict, run_manifest: Mapping[str, Any], failures: list[str],
|
|
277
|
+
) -> None:
|
|
278
|
+
"""수정 라운드 기록을 실제 dispatch 와, 마지막 라운드 verdict 를 verifierResults 와 맞춘다.
|
|
279
|
+
|
|
280
|
+
`_implementation-deliverable.md` "Correction round": verifierResults 는 마지막 라운드의
|
|
281
|
+
verdict 이고 앞선 FAIL 은 correctionRounds 에만 남는다. 원장에 있는 라운드가 기록에서
|
|
282
|
+
빠지면 그 FAIL 이 보고서에서 사라진다.
|
|
283
|
+
"""
|
|
284
|
+
implementation = data.get("implementation")
|
|
285
|
+
if not isinstance(implementation, dict):
|
|
286
|
+
return
|
|
287
|
+
rows = [row for row in implementation.get("correctionRounds") or [] if isinstance(row, dict)]
|
|
288
|
+
recorded = [row.get("round") for row in rows]
|
|
289
|
+
dispatched = sorted({
|
|
290
|
+
number for invocation in run_manifest.get("invocations") or []
|
|
291
|
+
if isinstance(invocation, Mapping)
|
|
292
|
+
and (number := correction_round(str(invocation.get("dispatchKind") or ""))) is not None
|
|
293
|
+
})
|
|
294
|
+
if sorted(recorded) != dispatched:
|
|
295
|
+
failures.append(
|
|
296
|
+
f"{_CORRECTION_BLOCKING} implementation.correctionRounds records rounds "
|
|
297
|
+
f"{sorted(recorded)} but the run dispatched correction rounds {dispatched}. "
|
|
298
|
+
"Record one entry per dispatched round, with its re-verification verdicts."
|
|
299
|
+
)
|
|
300
|
+
if recorded != list(range(1, len(recorded) + 1)):
|
|
301
|
+
failures.append(
|
|
302
|
+
f"{_CORRECTION_BLOCKING} implementation.correctionRounds rounds are {recorded}; "
|
|
303
|
+
"they run 1, 2 in order."
|
|
304
|
+
)
|
|
305
|
+
for row in rows:
|
|
306
|
+
verdicts = [v for v in row.get("verifierVerdicts") or [] if isinstance(v, dict)]
|
|
307
|
+
any_fail = any(v.get("verdict") == "FAIL" for v in verdicts)
|
|
308
|
+
if (row.get("result") == "pass") == any_fail:
|
|
309
|
+
failures.append(
|
|
310
|
+
f"{_CORRECTION_BLOCKING} implementation.correctionRounds round "
|
|
311
|
+
f"{row.get('round')} records `result: {row.get('result')}` but its "
|
|
312
|
+
f"re-verification verdicts are {[v.get('verdict') for v in verdicts]}; "
|
|
313
|
+
"`pass` means no verdict is FAIL."
|
|
314
|
+
)
|
|
315
|
+
if not rows:
|
|
316
|
+
return
|
|
317
|
+
last = {
|
|
318
|
+
str(v.get("verifier")): v.get("verdict")
|
|
319
|
+
for v in rows[-1].get("verifierVerdicts") or [] if isinstance(v, dict)
|
|
320
|
+
}
|
|
321
|
+
reported = {who: row.get("verdict") for who, row in _verifier_rows(data)}
|
|
322
|
+
if last != reported:
|
|
323
|
+
failures.append(
|
|
324
|
+
f"{_CORRECTION_BLOCKING} implementation.verifierResults verdicts {reported} differ "
|
|
325
|
+
f"from the last correction round's {last}. verifierResults carries the last "
|
|
326
|
+
"round's verdicts; earlier ones stay in correctionRounds."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
269
330
|
def _validate_verifier_fail_blocks_verdict(data: dict, failures: list[str]) -> None:
|
|
270
331
|
"""A verifier FAIL cannot be dropped during synthesis.
|
|
271
332
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"allowed": " - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n - the lead writes the
|
|
2
|
+
"allowed": " - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n - the lead writes the report narrative at the run manifest's `reportNarrativePath` directly (no `Report writer worker` dispatch); `okstra report-finalize` assembles the final report from it",
|
|
3
3
|
"forbidden": [
|
|
4
|
-
"entering this phase when
|
|
4
|
+
"entering this phase when a cited final-verification verdict is not release-ready — `Verdict Token` `blocked` or any token other than `accepted` / `conditional-accept`, a `conditional-accept` with no Conditional Acceptance Condition row or with any row whose `blocksReleaseHandoff` is not `false` (`okstra_ctl.release_gate.release_handoff_allowed`) — or when no final-verification report is cited",
|
|
5
5
|
"local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`), and any direct `git merge` / `git rebase` / `git rebase --onto` / `git cherry-pick` / `git commit --amend` run by the lead. Runtime exceptions are dependency-ordered rebases by `okstra_ctl.stage_rebase` (which invalidate verification and stop PR delivery), and merge commits on a dedicated multi-predecessor `merge-base` branch. Never merge a moving predecessor or origin branch into a downstream stage to update its base.",
|
|
6
6
|
"any git push variant that rewrites remote history, regardless of intent or whether the user said \"force it\": `git push --force`, `git push --force-with-lease`, `git push -f`, `git push +<refspec>`, or any other history-rewriting invocation",
|
|
7
7
|
"pushing directly to a release base branch — i.e. `git push origin <branch>` where `<branch>` is `main`, `master`, `prod`, `preprod`, `staging`, `dev`, or the branch the user chose as the release base in this run. The only permitted push targets are the branches `okstra handoff pr-plan` listed for this run (each stage `head_branch`, and any `merge-base` branch).",
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
the `releaseBase` the user picked. The report is the only place a reader learns which stages
|
|
5
5
|
shipped and in what order their PRs must be merged — the branch names do not say.
|
|
6
6
|
- Purpose: take a release-ready single-stage final-verification verdict for each already-committed implementation stage and turn it into a delivered push and/or pull request, with explicit user selection at every delivery step. **One stage is one PR.** The PR head is that stage's stack branch (`<prefix>-<task-id>-s<N>`); nothing is squashed or collected into a bundle branch. Runtime dependency rebases happen before delivery and invalidate the prior verification.
|
|
7
|
-
- **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT dispatch teammates, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The host-native Okstra lead performs every step inline (drafting PR text, asking the user, running git / gh, writing the
|
|
7
|
+
- **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT dispatch teammates, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The host-native Okstra lead performs every step inline (drafting PR text, asking the user, running git / gh, writing the report narrative) — see "Lead-only contract" below.
|
|
8
8
|
- Worker roster: none — `profile.json` declares no roles; the run is executed entirely by the Okstra lead.
|
|
9
9
|
- Lead-only contract (replaces the shared team contract for this phase):
|
|
10
10
|
- The host-native Okstra lead is the sole agent for this run. No worker dispatch, no teammates, no parallel sub-agents, no convergence loop. Do NOT run `okstra convergence` in any form: prepare already wrote this run's convergence state as "not run — fewer than two analysers", which is the record report assembly reads. **Enforced:** `okstra_ctl.render._initialize_lead_only_convergence` writes it when the run has no analysers, and leaves an existing state alone.
|
|
11
11
|
- The lead drafts each stage's PR title and PR body **inline** by reading the run brief, that stage's cited final-verification report, `git log --oneline <stage base>..<stage head>`, and `git diff <stage base>..<stage head> --stat`. No drafter worker is dispatched.
|
|
12
|
-
- The lead authors the
|
|
12
|
+
- The lead authors the report narrative directly (no `Report writer worker` dispatch) and never writes `final-report-*.data.json`. Write the narrative to the path the run manifest names in `reportNarrativePath`. It carries only the writer-owned fields (`humanSummary`, `summary`, `verdictCard`, `finalVerdict`, `rationale`, `recommendedNextSteps`, `followUpTasks`, and `releaseHandoff` with its `userNarrative`) in the narrative line grammar of `report_narrative.NARRATIVE_GRAMMAR_INSTRUCTIONS`: the first line is `# OKSTRA Report Narrative`, followed only by `- **Field Name**`, `- Item <N>` and `> value` lines, each value indented two spaces deeper than its label. `okstra report-finalize` assembles `data.json` from this narrative plus the approval, activity, team-state and convergence ledgers prepare already created. A missing or unparseable narrative fails the `project-activity` step with `owner=lead`. **Enforced:** `report_inputs._LEAD_AUTHORED_NARRATIVE_TASK_TYPES`, read by `report_assembly.assemble_report`.
|
|
13
13
|
- The shared anti-escalation rule from the common contract still applies: do not start any other lifecycle phase from inside this run.
|
|
14
14
|
- The shared "authority & permissions assumption" rule from the common contract still applies: assume the user holds every permission needed; do not block on hypothetical approvals.
|
|
15
15
|
- Pre-handoff entry gate (mandatory — refuse to start if any item fails):
|
|
@@ -33,7 +33,7 @@ When a predecessor advances, `okstra_ctl.stage_rebase.synchronize_stage_stack` r
|
|
|
33
33
|
|
|
34
34
|
Because the PRs are a stack, they must be merged in dependency order with a merge commit or a rebase-merge. A squash replaces the commits the next PR's base points at, so the stack breaks — every PR body states this, and the lead never merges.
|
|
35
35
|
|
|
36
|
-
This phase has no worker dispatch. It does not use a provider or report-writer roster; the host-native Okstra lead performs git/gh inspection, user questions, the PR draft, and the final report
|
|
36
|
+
This phase has no worker dispatch. It does not use a provider or report-writer roster; the host-native Okstra lead performs git/gh inspection, user questions, the PR draft, and the report narrative inline; `okstra report-finalize` assembles the final report from that narrative.
|
|
37
37
|
|
|
38
38
|
## 2. okstra-run wizard flow
|
|
39
39
|
|
|
@@ -762,7 +762,7 @@ def _attach_machine_planning(
|
|
|
762
762
|
_sync_human_summary_with_plan_body_gate(data, projection)
|
|
763
763
|
|
|
764
764
|
|
|
765
|
-
def _schema_owner(error: str) -> str:
|
|
765
|
+
def _schema_owner(error: str, narrative_owner: str) -> str:
|
|
766
766
|
field = error.split(" ", 1)[0]
|
|
767
767
|
owners = {
|
|
768
768
|
"executionStatus": "team-state",
|
|
@@ -781,8 +781,8 @@ def _schema_owner(error: str) -> str:
|
|
|
781
781
|
if "planBodyVerification" in error:
|
|
782
782
|
return "convergence"
|
|
783
783
|
if "selectedDirectionRef" in error:
|
|
784
|
-
return
|
|
785
|
-
return owners.get(field.split(".", 1)[0],
|
|
784
|
+
return narrative_owner
|
|
785
|
+
return owners.get(field.split(".", 1)[0], narrative_owner)
|
|
786
786
|
|
|
787
787
|
|
|
788
788
|
def _publish(
|
|
@@ -864,7 +864,7 @@ def assemble_report(
|
|
|
864
864
|
path_errors = planned_path_declaration_errors(data)
|
|
865
865
|
if path_errors:
|
|
866
866
|
raise ReportAssemblyError(tuple(
|
|
867
|
-
AssemblyIssue("
|
|
867
|
+
AssemblyIssue(inputs["narrative"].owner, str(inputs["narrative"].path), error.split(":", 1)[0], error)
|
|
868
868
|
for error in path_errors
|
|
869
869
|
))
|
|
870
870
|
direction_errors = selected_direction_plan_errors(
|
|
@@ -888,7 +888,7 @@ def assemble_report(
|
|
|
888
888
|
raise ReportAssemblyError(
|
|
889
889
|
tuple(
|
|
890
890
|
AssemblyIssue(
|
|
891
|
-
_schema_owner(error),
|
|
891
|
+
_schema_owner(error, inputs["narrative"].owner),
|
|
892
892
|
str(target),
|
|
893
893
|
error.split(" ", 1)[0],
|
|
894
894
|
error,
|
|
@@ -28,6 +28,10 @@ _INPUT_FIELDS = (
|
|
|
28
28
|
("convergence", "convergence", "convergenceStatePath"),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
# 워커 없이 리드가 리포트까지 쓰는 phase. 서사의 저자가 리드이므로, 서사가
|
|
32
|
+
# 없을 때 조립이 report-writer 를 지목하면 띄울 수 없는 워커를 찾게 된다.
|
|
33
|
+
_LEAD_AUTHORED_NARRATIVE_TASK_TYPES = frozenset({"release-handoff"})
|
|
34
|
+
|
|
31
35
|
_PLANNING_INPUT_FIELDS = (
|
|
32
36
|
("design-preparation", "design-surface-detector", "designPreparationPath"),
|
|
33
37
|
("plan-body-verification", "convergence", "planBodyVerificationPath"),
|
|
@@ -63,8 +67,13 @@ def report_input_paths(
|
|
|
63
67
|
if manifest.get("taskType") == "implementation-planning"
|
|
64
68
|
else ()
|
|
65
69
|
)
|
|
70
|
+
lead_authored = manifest.get("taskType") in _LEAD_AUTHORED_NARRATIVE_TASK_TYPES
|
|
66
71
|
rows = tuple(
|
|
67
|
-
ReportInputPath(
|
|
72
|
+
ReportInputPath(
|
|
73
|
+
key,
|
|
74
|
+
"lead" if lead_authored and key == "narrative" else owner,
|
|
75
|
+
_project_path(project_root, manifest.get(field), field),
|
|
76
|
+
)
|
|
68
77
|
for key, owner, field in fields
|
|
69
78
|
)
|
|
70
79
|
if len({row.key for row in rows}) != len(rows):
|