okstra 0.165.2 → 0.165.4
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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +3 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +22 -15
- package/runtime/prompts/profiles/_implementation-verifier.md +4 -4
- package/runtime/prompts/profiles/_stage-discipline.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
- package/runtime/python/okstra_ctl/cmux.py +9 -7
- package/runtime/python/okstra_ctl/consumers.py +73 -24
- package/runtime/python/okstra_ctl/dispatch_core.py +3 -3
- package/runtime/python/okstra_ctl/dispatch_state.py +21 -1
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +17 -5
- package/runtime/python/okstra_ctl/implementation_stage.py +2 -1
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +104 -12
- package/runtime/python/okstra_ctl/path_hints.py +10 -2
- package/runtime/python/okstra_ctl/render.py +5 -0
- package/runtime/python/okstra_ctl/stage_map.py +10 -4
- package/runtime/python/okstra_ctl/team.py +30 -6
- package/runtime/python/okstra_ctl/wizard.py +19 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +24 -4
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +18 -1
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +13 -2
- package/runtime/python/okstra_ctl/worktree.py +17 -1
- package/runtime/schemas/final-report-v1.0.schema.json +4 -0
- package/runtime/schemas/final-report-v2.0.schema.json +4 -0
- package/runtime/templates/implementation-worker-preamble.md +10 -3
- package/runtime/templates/reports/final-report.template.md +3 -0
- package/runtime/templates/worker-prompt-preamble.md +4 -3
- package/runtime/validators/validate-implementation-plan-stages.py +19 -2
- package/runtime/validators/validate-run.py +7 -0
- package/src/commands/execute/wizard.mjs +20 -8
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import tempfile
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from enum import Enum
|
|
@@ -20,7 +21,12 @@ from .worker_prompt_body import (
|
|
|
20
21
|
)
|
|
21
22
|
from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
|
|
22
23
|
from .worker_prompt_headers import worker_prompt_headers
|
|
23
|
-
from .worker_prompt_policy import
|
|
24
|
+
from .worker_prompt_policy import (
|
|
25
|
+
APPROVED_PLAN_HEADER,
|
|
26
|
+
IMPLEMENTATION_STAGE_HEADER,
|
|
27
|
+
PromptPlan,
|
|
28
|
+
resolve_prompt_plan_for_manifest,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
class PromptDeliveryMode(str, Enum):
|
|
@@ -94,6 +100,7 @@ _CLARIFICATION_AUTHORITY_INTRO = (
|
|
|
94
100
|
)
|
|
95
101
|
_CLARIFICATION_SOURCE_PREFIX = "Source:"
|
|
96
102
|
_REQUIRED_PROMPT_RESOURCES_HEADING = "## Required prompt resources"
|
|
103
|
+
_HTML_COMMENTS = re.compile(r"<!--.*?-->\n?", re.DOTALL)
|
|
97
104
|
|
|
98
105
|
|
|
99
106
|
@dataclass(frozen=True)
|
|
@@ -323,7 +330,7 @@ def _render_prompt(
|
|
|
323
330
|
lines.append(
|
|
324
331
|
f"**Prompt Delivery Mode:** {context.request.delivery_mode.value}"
|
|
325
332
|
)
|
|
326
|
-
lines.extend(
|
|
333
|
+
lines.extend(_implementation_anchor_lines(context, item.plan))
|
|
327
334
|
lines.extend(_prompt_body(context, state, worker, item.plan))
|
|
328
335
|
lines.extend(_resource_lines(context, item))
|
|
329
336
|
except InitialPromptMaterializationError:
|
|
@@ -467,12 +474,27 @@ def _resolve_prompt_resources(
|
|
|
467
474
|
return tuple(
|
|
468
475
|
_PromptResource(
|
|
469
476
|
path=path.resolve(),
|
|
470
|
-
text=
|
|
477
|
+
text=_worker_visible_text(
|
|
478
|
+
_read_required_nonempty_text(path, "required prompt resource")
|
|
479
|
+
),
|
|
471
480
|
)
|
|
472
481
|
for path in paths
|
|
473
482
|
)
|
|
474
483
|
|
|
475
484
|
|
|
485
|
+
def _worker_visible_text(text: str) -> str:
|
|
486
|
+
"""Strip lead-and-maintainer-only passages before a body reaches a worker.
|
|
487
|
+
|
|
488
|
+
These sidecars have two readers: the lead lazy-reads the file to run the
|
|
489
|
+
phase, and the worker receives the same body inlined in its prompt. An
|
|
490
|
+
HTML comment is the seam between them — rendered markdown hides it, so it
|
|
491
|
+
holds the delivery plumbing (which path feeds this file, which heading the
|
|
492
|
+
CLI wrapper greps for) that the worker would otherwise pay for and read as
|
|
493
|
+
an instruction addressed to itself.
|
|
494
|
+
"""
|
|
495
|
+
return _HTML_COMMENTS.sub("", text).lstrip("\n")
|
|
496
|
+
|
|
497
|
+
|
|
476
498
|
def _required_resource_path_candidates(
|
|
477
499
|
context: _MaterializationContext,
|
|
478
500
|
plan: PromptPlan,
|
|
@@ -481,15 +503,17 @@ def _required_resource_path_candidates(
|
|
|
481
503
|
if plan.audience == "implementation-executor":
|
|
482
504
|
paths = (
|
|
483
505
|
_executor_profile_path(context),
|
|
506
|
+
profiles / "_stage-discipline.md",
|
|
484
507
|
profiles / "_coding-conventions-preflight.md",
|
|
485
508
|
profiles / "_implementation-diff-review.md",
|
|
486
509
|
profiles / "_implementation-self-check.md",
|
|
487
510
|
)
|
|
488
511
|
elif plan.audience == "implementation-verifier":
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
512
|
+
# The self-check body is executor-only: it is written to the worker that
|
|
513
|
+
# owns the diff ("fix it or surface the violation", break-and-restore
|
|
514
|
+
# mutation checks), and a verifier is barred from every edit it asks
|
|
515
|
+
# for. Its own blocking taxonomy is inlined in the verifier sidecar.
|
|
516
|
+
paths = (profiles / "_implementation-verifier.md",)
|
|
493
517
|
else:
|
|
494
518
|
return ()
|
|
495
519
|
return paths
|
|
@@ -873,15 +897,35 @@ def _result_paths(
|
|
|
873
897
|
return str(final_report_data_path(Path(expected_report))), worker_result
|
|
874
898
|
|
|
875
899
|
|
|
876
|
-
def
|
|
900
|
+
def _implementation_anchor_lines(
|
|
877
901
|
context: _MaterializationContext,
|
|
878
902
|
plan: PromptPlan,
|
|
879
903
|
) -> list[str]:
|
|
904
|
+
"""Render the anchors only an implementation audience carries.
|
|
905
|
+
|
|
906
|
+
The approved plan and the stage number are absolute here because a CLI
|
|
907
|
+
worker's cwd is the stage worktree, where a `.okstra/...` relative path
|
|
908
|
+
resolves against the wrong root.
|
|
909
|
+
"""
|
|
880
910
|
if plan.audience not in {
|
|
881
911
|
"implementation-executor",
|
|
882
912
|
"implementation-verifier",
|
|
883
913
|
}:
|
|
884
914
|
return []
|
|
915
|
+
worktree = _required_worktree_path(context)
|
|
916
|
+
lines = [f"**Worktree:** {worktree}"]
|
|
917
|
+
if plan.audience == "implementation-executor":
|
|
918
|
+
lines.append(f"cwd for every mutating command: {worktree}")
|
|
919
|
+
lines.append(
|
|
920
|
+
f"{APPROVED_PLAN_HEADER} {_required_approved_plan_path(context)}"
|
|
921
|
+
)
|
|
922
|
+
lines.append(
|
|
923
|
+
f"{IMPLEMENTATION_STAGE_HEADER} {_required_stage_number(context)}"
|
|
924
|
+
)
|
|
925
|
+
return lines
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _required_worktree_path(context: _MaterializationContext) -> str:
|
|
885
929
|
executor_worktree = context.active_context.get("executorWorktree")
|
|
886
930
|
value = (
|
|
887
931
|
_string_value(executor_worktree.get("path"))
|
|
@@ -893,10 +937,58 @@ def _worktree_lines(
|
|
|
893
937
|
"required_input_missing",
|
|
894
938
|
"implementation prompt generation requires a worktree path",
|
|
895
939
|
)
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
940
|
+
return value
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _required_approved_plan_path(context: _MaterializationContext) -> str:
|
|
944
|
+
"""Read the approved plan path from this run's user-input snapshot.
|
|
945
|
+
|
|
946
|
+
run-inputs owns it because it is a user input, not a rendered path — and
|
|
947
|
+
reading it there also keeps runs prepared before this anchor existed
|
|
948
|
+
materializable, since every run has always recorded it.
|
|
949
|
+
"""
|
|
950
|
+
source_artifacts = context.active_context.get("sourceArtifacts")
|
|
951
|
+
run_inputs_rel = (
|
|
952
|
+
_string_value(source_artifacts.get("runInputsPath"))
|
|
953
|
+
if isinstance(source_artifacts, Mapping)
|
|
954
|
+
else ""
|
|
955
|
+
)
|
|
956
|
+
if not run_inputs_rel:
|
|
957
|
+
raise InitialPromptMaterializationError(
|
|
958
|
+
"required_input_missing",
|
|
959
|
+
"implementation prompt generation requires a run inputs path",
|
|
960
|
+
)
|
|
961
|
+
payload = _load_json_object(
|
|
962
|
+
_resolve_input_path(context.project_root, Path(run_inputs_rel)),
|
|
963
|
+
"run inputs",
|
|
964
|
+
)
|
|
965
|
+
inputs = payload.get("inputs")
|
|
966
|
+
value = (
|
|
967
|
+
_string_value(inputs.get("approvedPlanPath"))
|
|
968
|
+
if isinstance(inputs, Mapping)
|
|
969
|
+
else ""
|
|
970
|
+
)
|
|
971
|
+
if not value:
|
|
972
|
+
raise InitialPromptMaterializationError(
|
|
973
|
+
"required_input_missing",
|
|
974
|
+
"implementation prompt generation requires an approved plan path",
|
|
975
|
+
)
|
|
976
|
+
return str(_resolve_input_path(context.project_root, Path(value)))
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _required_stage_number(context: _MaterializationContext) -> str:
|
|
980
|
+
value = _string_value(_active_run_field(context, "stage"))
|
|
981
|
+
if not value:
|
|
982
|
+
raise InitialPromptMaterializationError(
|
|
983
|
+
"required_input_missing",
|
|
984
|
+
"implementation prompt generation requires a stage number",
|
|
985
|
+
)
|
|
986
|
+
return value
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _active_run_field(context: _MaterializationContext, key: str) -> Any:
|
|
990
|
+
run = context.active_context.get("run")
|
|
991
|
+
return run.get(key) if isinstance(run, Mapping) else ""
|
|
900
992
|
|
|
901
993
|
|
|
902
994
|
def _prompt_plan(
|
|
@@ -95,7 +95,7 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
95
95
|
"kind": ACTIVE_CONTEXT_KIND,
|
|
96
96
|
"task": _hydrate_active_task(payload, ctx),
|
|
97
97
|
"workflow": dict(_mapping(payload.get("workflow"))),
|
|
98
|
-
"run": _hydrate_active_run(ctx),
|
|
98
|
+
"run": _hydrate_active_run(payload, ctx),
|
|
99
99
|
"instructionSet": _hydrate_active_instruction_set(payload, ctx),
|
|
100
100
|
"workers": _hydrate_active_workers(payload, ctx),
|
|
101
101
|
"errorLogs": _hydrate_active_error_logs(ctx),
|
|
@@ -182,8 +182,16 @@ def _hydrate_active_task(payload: Mapping[str, Any], ctx: Mapping[str, str]) ->
|
|
|
182
182
|
return task
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
def _hydrate_active_run(
|
|
185
|
+
def _hydrate_active_run(
|
|
186
|
+
payload: Mapping[str, Any],
|
|
187
|
+
ctx: Mapping[str, str],
|
|
188
|
+
) -> dict[str, str]:
|
|
189
|
+
# `stage` is a run input, not a derived path, so pathHints cannot rebuild
|
|
190
|
+
# it — it survives the round trip only by being read back off the compact
|
|
191
|
+
# payload.
|
|
192
|
+
run = _mapping(payload.get("run"))
|
|
186
193
|
return {
|
|
194
|
+
"stage": str(run.get("stage", "") or ""),
|
|
187
195
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
188
196
|
"runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
189
197
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
@@ -506,6 +506,11 @@ def _active_workflow(ctx: dict) -> dict:
|
|
|
506
506
|
|
|
507
507
|
def _active_run(ctx: dict) -> dict:
|
|
508
508
|
return {
|
|
509
|
+
# `implementation` binds one run to one Stage Map stage, and the
|
|
510
|
+
# executor prompt has to name it: its sidecar forbids recomputing the
|
|
511
|
+
# stage from `consumers.jsonl`. Feeds the implementation prompt anchor
|
|
512
|
+
# in `initial_prompt_materialization`.
|
|
513
|
+
"stage": ctx.get("RUN_STAGE", ""),
|
|
509
514
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
510
515
|
"runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
|
|
511
516
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
@@ -367,8 +367,14 @@ def parse_stage_map_file(markdown_path: Path) -> list[StageMapStage]:
|
|
|
367
367
|
return _parse_schema_v2_stage_map(data, str(data_path))
|
|
368
368
|
|
|
369
369
|
|
|
370
|
-
def
|
|
371
|
-
"""The schema-v2 sidecar as a whole, `{}` for a v1 report.
|
|
370
|
+
def schema_v2_report(markdown_path: Path) -> dict[str, Any]:
|
|
371
|
+
"""The schema-v2 sidecar as a whole, `{}` for a v1 report.
|
|
372
|
+
|
|
373
|
+
Public because every caller that must branch on report schema needs it —
|
|
374
|
+
including `validators/validate-implementation-plan-stages.py`, which is a
|
|
375
|
+
separate process and cannot reach a private helper without copying the
|
|
376
|
+
sidecar-detection rule and letting the two drift.
|
|
377
|
+
"""
|
|
372
378
|
data_path = Path(markdown_path).resolve().with_suffix(".data.json")
|
|
373
379
|
if not data_path.exists():
|
|
374
380
|
return {}
|
|
@@ -383,7 +389,7 @@ def _schema_v2_report(markdown_path: Path) -> dict[str, Any]:
|
|
|
383
389
|
|
|
384
390
|
def _planning_section(markdown_path: Path) -> dict[str, Any]:
|
|
385
391
|
"""The report's `implementationPlanning` block, `{}` for v1."""
|
|
386
|
-
planning =
|
|
392
|
+
planning = schema_v2_report(markdown_path).get("implementationPlanning")
|
|
387
393
|
return planning if isinstance(planning, dict) else {}
|
|
388
394
|
|
|
389
395
|
|
|
@@ -407,7 +413,7 @@ def _stage_narratives(value: Any) -> dict[int, dict[str, Any]]:
|
|
|
407
413
|
|
|
408
414
|
def load_planning_detail(markdown_path: Path) -> PlanningDetail:
|
|
409
415
|
"""Read one report's narrative rows; empty for a schema-v1 report."""
|
|
410
|
-
report =
|
|
416
|
+
report = schema_v2_report(markdown_path)
|
|
411
417
|
planning = report.get("implementationPlanning")
|
|
412
418
|
if not isinstance(planning, dict) or not planning:
|
|
413
419
|
return PlanningDetail({}, {})
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
11
|
import json
|
|
12
|
+
import subprocess
|
|
12
13
|
import sys
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Mapping, Sequence
|
|
@@ -202,11 +203,11 @@ def _reclaimable_panes(
|
|
|
202
203
|
) -> list[dict[str, str]]:
|
|
203
204
|
"""Everything this run owns and may close.
|
|
204
205
|
|
|
205
|
-
Under cmux the recorded ids are the
|
|
206
|
-
to sweep with, and scanning by title would be worse than nothing: cmux
|
|
207
|
-
its own agent surfaces with the same glyph okstra's tmux cleanup
|
|
208
|
-
teammate marker, so a sweep could close the lead. Only surfaces
|
|
209
|
-
created are recorded, so only those can be closed.
|
|
206
|
+
Under cmux the recorded ids are the only candidates. There is no per-pane tag
|
|
207
|
+
API to sweep with, and scanning by title would be worse than nothing: cmux
|
|
208
|
+
labels its own agent surfaces with the same glyph okstra's tmux cleanup
|
|
209
|
+
treats as a teammate marker, so a sweep could close the lead. Only surfaces
|
|
210
|
+
okstra created are recorded, so only those can be closed.
|
|
210
211
|
"""
|
|
211
212
|
seen: set[str] = set()
|
|
212
213
|
panes: list[dict[str, str]] = []
|
|
@@ -214,13 +215,36 @@ def _reclaimable_panes(
|
|
|
214
215
|
if isinstance(record, dict):
|
|
215
216
|
_append_pane(panes, seen, str(record.get("paneId", "")), "worker")
|
|
216
217
|
if _is_cmux_run(manifest):
|
|
217
|
-
return panes
|
|
218
|
+
return _still_open_surfaces(panes)
|
|
218
219
|
lead_pane = tmux.resolve_caller_pane()
|
|
219
220
|
for pane in tmux.list_run_panes(run_dir, lead_pane=lead_pane):
|
|
220
221
|
_append_pane(panes, seen, pane.pane_id, pane.kind)
|
|
221
222
|
return panes
|
|
222
223
|
|
|
223
224
|
|
|
225
|
+
def _still_open_surfaces(panes: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
226
|
+
"""The recorded surfaces cmux still shows.
|
|
227
|
+
|
|
228
|
+
`workerDispatches` is append-only and nothing prunes it, so a surface closed
|
|
229
|
+
at an earlier round boundary stays recorded for the rest of the run. Taking
|
|
230
|
+
the ledger as the residual set makes the run-end cleanup gate offer to close
|
|
231
|
+
panes that left the screen rounds ago, on a workspace holding none.
|
|
232
|
+
|
|
233
|
+
An unreachable cmux keeps the ledger rather than reporting an empty set: a
|
|
234
|
+
wedged app is not evidence that the surfaces are gone, closing one that
|
|
235
|
+
already went away is a silent no-op, and skipping a live one strands it on
|
|
236
|
+
the user's screen for the rest of the session.
|
|
237
|
+
"""
|
|
238
|
+
workspace = cmux.resolve_lead_workspace()
|
|
239
|
+
if not workspace:
|
|
240
|
+
return panes
|
|
241
|
+
try:
|
|
242
|
+
open_ids = cmux.open_surface_ids(workspace)
|
|
243
|
+
except (RuntimeError, OSError, subprocess.SubprocessError):
|
|
244
|
+
return panes
|
|
245
|
+
return [pane for pane in panes if pane["paneId"] in open_ids]
|
|
246
|
+
|
|
247
|
+
|
|
224
248
|
def _append_pane(panes: list[dict[str, str]], seen: set[str], pane_id: str, kind: str) -> None:
|
|
225
249
|
if pane_id and pane_id not in seen:
|
|
226
250
|
panes.append({"paneId": pane_id, "kind": kind})
|
|
@@ -4484,11 +4484,28 @@ def _reset_from(state: WizardState, target_step: str) -> None:
|
|
|
4484
4484
|
idx = next((i for i, s in enumerate(STEPS) if s.id == target_step), -1)
|
|
4485
4485
|
if idx < 0:
|
|
4486
4486
|
return
|
|
4487
|
+
# A later step may own a field an earlier answered step also owns —
|
|
4488
|
+
# `handoff_stage_pick` owns `approved_plan_path` because it resolves the
|
|
4489
|
+
# plan on its own. Clearing it while rewinding to a step in front of it
|
|
4490
|
+
# drops an answer the user never revisited; the rewound step then fails its
|
|
4491
|
+
# own `applies` guard, so no step is left to ask and the wizard reports
|
|
4492
|
+
# done while `outcome` still refuses it as incomplete.
|
|
4493
|
+
owned_earlier = {
|
|
4494
|
+
fname
|
|
4495
|
+
for step in STEPS[:idx]
|
|
4496
|
+
if step.id in state.answered
|
|
4497
|
+
for fname in step.owns
|
|
4498
|
+
}
|
|
4487
4499
|
cleared_ids: set[str] = set()
|
|
4488
|
-
for step in STEPS[idx:]:
|
|
4500
|
+
for position, step in enumerate(STEPS[idx:]):
|
|
4489
4501
|
cleared_ids.add(step.id)
|
|
4502
|
+
is_rewind_target = position == 0
|
|
4490
4503
|
for fname in step.owns:
|
|
4491
|
-
|
|
4504
|
+
# The target's own fields always clear — that is the answer the
|
|
4505
|
+
# user came back to replace, even when an earlier step declares it
|
|
4506
|
+
# too (`task_pick` derives `task_type` for an existing task).
|
|
4507
|
+
if is_rewind_target or fname not in owned_earlier:
|
|
4508
|
+
_reset_field(state, fname)
|
|
4492
4509
|
state.answered = [a for a in state.answered if a not in cleared_ids]
|
|
4493
4510
|
direct_input_pending = {
|
|
4494
4511
|
S_FEATURE_EVIDENCE: "feature_evidence_pending_text",
|
|
@@ -11,6 +11,21 @@ ANALYSIS_WORKER_LABELS = {
|
|
|
11
11
|
"codex": "Codex worker",
|
|
12
12
|
"antigravity": "Antigravity worker",
|
|
13
13
|
}
|
|
14
|
+
# An implementation audience shares this body but not its premise: the executor
|
|
15
|
+
# owns the diff and the verifier grades it, so neither is producing one of the
|
|
16
|
+
# independent findings that cross-verification triangulates.
|
|
17
|
+
ROLE_STATEMENTS = {
|
|
18
|
+
"executor": (
|
|
19
|
+
"You are the Executor for this implementation stage — the only worker "
|
|
20
|
+
"permitted to mutate project files. Carry the stage end to end and "
|
|
21
|
+
"produce the worker result."
|
|
22
|
+
),
|
|
23
|
+
"verifier": (
|
|
24
|
+
"You are a verifier for this implementation stage. Reproduce its QA "
|
|
25
|
+
"yourself, stay read-only on project files, and return an independent "
|
|
26
|
+
"verdict."
|
|
27
|
+
),
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
|
|
16
31
|
def analysis_prompt_body(
|
|
@@ -30,10 +45,7 @@ def analysis_prompt_body(
|
|
|
30
45
|
f"# {label} Dispatch",
|
|
31
46
|
"",
|
|
32
47
|
"## Role",
|
|
33
|
-
(
|
|
34
|
-
f"You are the {label} for okstra cross-verification. "
|
|
35
|
-
"Produce an independent worker result."
|
|
36
|
-
),
|
|
48
|
+
_role_statement(label, role),
|
|
37
49
|
"",
|
|
38
50
|
"## Task",
|
|
39
51
|
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
@@ -52,6 +64,14 @@ def analysis_prompt_body(
|
|
|
52
64
|
]
|
|
53
65
|
|
|
54
66
|
|
|
67
|
+
def _role_statement(label: str, role: str) -> str:
|
|
68
|
+
return ROLE_STATEMENTS.get(
|
|
69
|
+
role,
|
|
70
|
+
f"You are the {label} for okstra cross-verification. "
|
|
71
|
+
"Produce an independent worker result.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
55
75
|
def analysis_input_lines(
|
|
56
76
|
manifest: Mapping[str, Any],
|
|
57
77
|
active_context: Mapping[str, Any],
|
|
@@ -8,6 +8,7 @@ from typing import Any, Mapping, Sequence
|
|
|
8
8
|
|
|
9
9
|
from .worker_prompt_policy import (
|
|
10
10
|
ERRORS_PATH_HEADERS,
|
|
11
|
+
IMPLEMENTATION_HEADERS,
|
|
11
12
|
PromptPlan,
|
|
12
13
|
resolve_prompt_plan_for_manifest,
|
|
13
14
|
)
|
|
@@ -48,10 +49,11 @@ _NON_BODY_PREFIXES = (
|
|
|
48
49
|
"Assigned worker prompt history path:",
|
|
49
50
|
"**Worker Preamble Path:**",
|
|
50
51
|
_EVIDENCE_LEDGER_HEADER_PREFIX,
|
|
52
|
+
"**Evidence citations:**",
|
|
51
53
|
*ERRORS_PATH_HEADERS,
|
|
52
54
|
"**Read scope:**",
|
|
53
55
|
"**File write mode:**",
|
|
54
|
-
|
|
56
|
+
*IMPLEMENTATION_HEADERS,
|
|
55
57
|
"**Verification scope:**",
|
|
56
58
|
"**Verification base ref:**",
|
|
57
59
|
"**Verification head ref:**",
|
|
@@ -38,6 +38,23 @@ READ_SCOPE_HEADER = (
|
|
|
38
38
|
)
|
|
39
39
|
EVIDENCE_LEDGER_HEADER = "**Evidence ledger:** required-v1"
|
|
40
40
|
|
|
41
|
+
# `required-v1` is a mode name; what it demands lives in the Worker Preamble's
|
|
42
|
+
# "Evidence read ledger". But the preamble reaches the worker as a *path* —
|
|
43
|
+
# eager-include inlines only the role sidecars — so a worker that never opens it
|
|
44
|
+
# gets the switch without the rule, and Phase 7 then fails its result over a
|
|
45
|
+
# citation form the prompt never stated. Restating it inline is what
|
|
46
|
+
# READ_SCOPE_HEADER does, for the same reason: the rule has to be in front of the
|
|
47
|
+
# worker before its first citation, not in a file it may never read.
|
|
48
|
+
EVIDENCE_CITATION_HEADER = (
|
|
49
|
+
"**Evidence citations:** Append one `- Evidence read: <project-relative "
|
|
50
|
+
"path, no line suffix>` row to the audit sidecar for every file you open as "
|
|
51
|
+
"claim evidence, and cite that file in your result with backticks, a line "
|
|
52
|
+
"suffix, and the identical project-relative path — `src/config/env.ts:1-22`, "
|
|
53
|
+
"never the bare filename `env.ts:1-22`. A bare filename does not match its "
|
|
54
|
+
"ledger row and fails exactly like a file you never opened, however many "
|
|
55
|
+
"times you cited the full path earlier."
|
|
56
|
+
)
|
|
57
|
+
|
|
41
58
|
# `agy`'s write tool validates the target against the Gemini artifact store
|
|
42
59
|
# whenever the model attaches ArtifactMetadata, and rejects every path outside
|
|
43
60
|
# `~/.gemini/antigravity-cli/brain/<uuid>/`. All okstra worker outputs live under
|
|
@@ -103,7 +120,7 @@ def worker_prompt_headers(
|
|
|
103
120
|
f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}"
|
|
104
121
|
)
|
|
105
122
|
if dispatch_kind == "initial" and plan.audience != "report-writer":
|
|
106
|
-
headers
|
|
123
|
+
headers += [EVIDENCE_LEDGER_HEADER, EVIDENCE_CITATION_HEADER]
|
|
107
124
|
headers.extend([
|
|
108
125
|
f"**Errors log path:** {errors_log_path}",
|
|
109
126
|
f"**Errors sidecar path:** {errors_sidecar_path}",
|
|
@@ -27,6 +27,17 @@ ERRORS_PATH_HEADERS = (
|
|
|
27
27
|
"**Errors log path:**",
|
|
28
28
|
"**Errors sidecar path:**",
|
|
29
29
|
)
|
|
30
|
+
# The executor sidecar tells the worker to execute exactly one Stage Map stage
|
|
31
|
+
# against the approved plan, and forbids recomputing that stage from
|
|
32
|
+
# `consumers.jsonl`. Both facts therefore have to reach the worker prompt
|
|
33
|
+
# itself: the lead's launch prompt carries them, and no worker reads that.
|
|
34
|
+
APPROVED_PLAN_HEADER = "**Approved plan:**"
|
|
35
|
+
IMPLEMENTATION_STAGE_HEADER = "**Stage for this implementation run:**"
|
|
36
|
+
IMPLEMENTATION_HEADERS = (
|
|
37
|
+
"**Worktree:**",
|
|
38
|
+
APPROVED_PLAN_HEADER,
|
|
39
|
+
IMPLEMENTATION_STAGE_HEADER,
|
|
40
|
+
)
|
|
30
41
|
FINAL_VERIFICATION_HEADERS = (
|
|
31
42
|
"**Worktree:**",
|
|
32
43
|
"**Verification scope:**",
|
|
@@ -96,14 +107,14 @@ def resolve_prompt_plan(
|
|
|
96
107
|
return _plan(
|
|
97
108
|
"implementation-executor",
|
|
98
109
|
allow_coding_preflight=True,
|
|
99
|
-
required_headers=
|
|
110
|
+
required_headers=IMPLEMENTATION_HEADERS,
|
|
100
111
|
)
|
|
101
112
|
if task_type == "implementation":
|
|
102
113
|
return _plan(
|
|
103
114
|
"implementation-verifier",
|
|
104
115
|
equality_group="implementation-verifier-core",
|
|
105
116
|
allow_coding_preflight=True,
|
|
106
|
-
required_headers=
|
|
117
|
+
required_headers=IMPLEMENTATION_HEADERS,
|
|
107
118
|
)
|
|
108
119
|
if task_type == "final-verification":
|
|
109
120
|
return _plan(
|
|
@@ -219,7 +219,7 @@ def resolve_stage_worktree_decision(
|
|
|
219
219
|
safe_task = _safe_segment(task_id_segment)
|
|
220
220
|
existing = worktree_registry.lookup(
|
|
221
221
|
safe_project, safe_group, safe_task, stage_number=stage_number)
|
|
222
|
-
if existing is not None and existing
|
|
222
|
+
if existing is not None and _stage_entry_is_reusable(existing):
|
|
223
223
|
return StageWorktreeDecision(
|
|
224
224
|
status="reused",
|
|
225
225
|
path=existing.worktree_path,
|
|
@@ -237,6 +237,22 @@ def resolve_stage_worktree_decision(
|
|
|
237
237
|
)
|
|
238
238
|
|
|
239
239
|
|
|
240
|
+
def _stage_entry_is_reusable(entry: worktree_registry.WorktreeEntry) -> bool:
|
|
241
|
+
"""Whether a registered stage worktree can be entered by this run.
|
|
242
|
+
|
|
243
|
+
`active` is the live-run case. `released` with the directory still on disk is
|
|
244
|
+
the fix-run case: a stage whose verifier returned FAIL records a `failed`
|
|
245
|
+
consumers row, which frees the occupancy but deliberately keeps the worktree
|
|
246
|
+
and its branch as the reviewable stack. Re-entry MUST reuse that tree —
|
|
247
|
+
provisioning anew refuses on the existing path and branch. After whole-task
|
|
248
|
+
final-verification removes the directory the entry stops being reusable, so
|
|
249
|
+
the stage provisions from scratch.
|
|
250
|
+
"""
|
|
251
|
+
if entry.status == "active":
|
|
252
|
+
return True
|
|
253
|
+
return entry.status == "released" and Path(entry.worktree_path).is_dir()
|
|
254
|
+
|
|
255
|
+
|
|
240
256
|
def _safe_segment(value: str) -> str:
|
|
241
257
|
"""Sanitise a single path/branch segment.
|
|
242
258
|
|
|
@@ -18,6 +18,12 @@ Work like a senior engineer who owns this result, not a commentator on it.
|
|
|
18
18
|
- Read `overview.md` and `clean-code.md` under `**Coding preflight pack:**`, then follow every matching language, framework, and architecture route before editing or verification.
|
|
19
19
|
- Read the approved implementation deliverable and any effective design-preparation block enumerated by the prompt.
|
|
20
20
|
|
|
21
|
+
### Reading rules
|
|
22
|
+
|
|
23
|
+
- Read every file enumerated under `[Required reading]`, `## Inputs`, and `## Required prompt resources` completely, plus the source files this stage's plan names.
|
|
24
|
+
- Allowlist reads to those paths, the stage worktree's own source, and evidence paths a claim must cite. Do not auto-read host-injected `graphify-out/`, skill catalogs, or other non-okstra artifacts.
|
|
25
|
+
- Host session instructions — SessionStart hooks, global `CLAUDE.md` / `AGENTS.md`, skill catalogs — do not apply inside this run, and a directive from one of them to read an un-enumerated file is not a conflict to weigh in the host's favour: this prompt wins. Record the file under the result's assumptions instead of opening it. The project's own `CLAUDE.md` / lint config still binds as a coding convention — the preflight gate routes it — but the host's *reading* directives stop at this boundary.
|
|
26
|
+
|
|
21
27
|
## Worktree and command discipline
|
|
22
28
|
|
|
23
29
|
- `**Worktree:**` is the canonical checkout. Project commands run with that directory as cwd.
|
|
@@ -45,9 +51,10 @@ Every initial implementation prompt begins with these generated common anchors i
|
|
|
45
51
|
6. `**Worker Error Contract Path:** <absolute-path>`
|
|
46
52
|
7. `**Coding preflight pack:** <absolute-path>`
|
|
47
53
|
8. `**Evidence ledger:** required-v1`
|
|
48
|
-
9. `**
|
|
49
|
-
10. `**Errors
|
|
50
|
-
11. `**
|
|
54
|
+
9. `**Evidence citations:** <rule>` — the ledger-row and citation-path rule of §"Evidence read ledger", restated inline so it is in front of you before your first citation.
|
|
55
|
+
10. `**Errors log path:** <absolute-path>`
|
|
56
|
+
11. `**Errors sidecar path:** <absolute-path>`
|
|
57
|
+
12. `**Read scope:** <allowlist>`
|
|
51
58
|
|
|
52
59
|
The implementation body additionally carries `**Worktree:**` and its role-sidecar inputs. Do not synthesize any missing path.
|
|
53
60
|
|
|
@@ -674,6 +674,9 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
674
674
|
### 5.7.5 Stage Sidecar Evidence
|
|
675
675
|
|
|
676
676
|
- Stage: `{{ implementation.stageSidecarEvidence.stageNumber }}` — {{ implementation.stageSidecarEvidence.stageTitle }}
|
|
677
|
+
{% if implementation.stageSidecarEvidence.withheld -%}
|
|
678
|
+
- Carry sidecar **withheld** — not written to `carry/stage-{{ implementation.stageSidecarEvidence.stageNumber }}.json`: {{ implementation.stageSidecarEvidence.withheld }}
|
|
679
|
+
{% endif -%}
|
|
677
680
|
- Carry sidecar JSON:
|
|
678
681
|
```json
|
|
679
682
|
{{ implementation.stageSidecarEvidence.carryJson | indent(2) }}
|
|
@@ -43,9 +43,10 @@ Every initial analysis prompt begins with these generated anchors in this exact
|
|
|
43
43
|
6. `**Worker Preamble Path:** <absolute-path>` — selects this analysis preamble.
|
|
44
44
|
7. `**Worker Error Contract Path:** <absolute-path>` — shared by every initial audience.
|
|
45
45
|
8. `**Evidence ledger:** required-v1`
|
|
46
|
-
9. `**
|
|
47
|
-
10. `**Errors
|
|
48
|
-
11. `**
|
|
46
|
+
9. `**Evidence citations:** <rule>` — the ledger-row and citation-path rule of §"Evidence read ledger", restated inline so it is in front of you before your first citation.
|
|
47
|
+
10. `**Errors log path:** <absolute-path>`
|
|
48
|
+
11. `**Errors sidecar path:** <absolute-path>`
|
|
49
|
+
12. `**Read scope:** <allowlist>`
|
|
49
50
|
|
|
50
51
|
`final-verification` additionally carries its six verification-target anchors. `improvement-discovery` carries `**Phase 1.5 Grilling Log:**`. Reverify prompts are lightweight and do not use this preamble.
|
|
51
52
|
|
|
@@ -25,6 +25,7 @@ from okstra_ctl.stage_map import ( # noqa: E402
|
|
|
25
25
|
StageMapError,
|
|
26
26
|
StageMapStage,
|
|
27
27
|
parse_stage_map_text,
|
|
28
|
+
schema_v2_report,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
HARD_STEP_CAP = 8
|
|
@@ -502,13 +503,29 @@ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
|
502
503
|
return errors
|
|
503
504
|
|
|
504
505
|
|
|
506
|
+
def collect_plan_errors(plan_path: Path) -> List[ValidationError]:
|
|
507
|
+
"""The S-checks for one approved plan, whichever schema wrote it.
|
|
508
|
+
|
|
509
|
+
A schema-v2 report keeps its stage map in the `.data.json` sidecar and
|
|
510
|
+
renders no `## 5.5 Stage Map` section, so scanning its markdown reports the
|
|
511
|
+
section as missing and blocks every run that approved such a plan.
|
|
512
|
+
"""
|
|
513
|
+
planning = schema_v2_report(plan_path).get("implementationPlanning")
|
|
514
|
+
if isinstance(planning, dict) and planning:
|
|
515
|
+
return collect_data_validation_errors(planning)
|
|
516
|
+
return collect_validation_errors(plan_path.read_text(encoding="utf-8"))
|
|
517
|
+
|
|
518
|
+
|
|
505
519
|
def main(argv: List[str]) -> int:
|
|
506
520
|
p = argparse.ArgumentParser()
|
|
507
521
|
p.add_argument("--plan", required=True)
|
|
508
522
|
args = p.parse_args(argv)
|
|
509
|
-
text = Path(args.plan).read_text(encoding="utf-8")
|
|
510
523
|
|
|
511
|
-
|
|
524
|
+
try:
|
|
525
|
+
errors = collect_plan_errors(Path(args.plan))
|
|
526
|
+
except StageMapError as exc:
|
|
527
|
+
print(f"S0 stage=0: {exc.reason}", file=sys.stderr)
|
|
528
|
+
return 1
|
|
512
529
|
if errors:
|
|
513
530
|
for e in errors:
|
|
514
531
|
print(f"{e.code} stage={e.stage}: {e.message}", file=sys.stderr)
|
|
@@ -5061,6 +5061,13 @@ def _validate_stage_carry_sidecar_exists(
|
|
|
5061
5061
|
stage = evidence.get("stageNumber")
|
|
5062
5062
|
if not isinstance(stage, int):
|
|
5063
5063
|
return
|
|
5064
|
+
# A stage whose verifier returned FAIL must NOT persist its carry: the carry
|
|
5065
|
+
# file is what marks the stage `done`, and doing that would stack the next
|
|
5066
|
+
# stage on a confirmed regression. Such a run states the reason in
|
|
5067
|
+
# `withheld` and records a `failed` consumers row instead, so the absent
|
|
5068
|
+
# file is the correct outcome, not a gap.
|
|
5069
|
+
if str(evidence.get("withheld") or "").strip():
|
|
5070
|
+
return
|
|
5064
5071
|
# Carry sidecars are stage-SHARED: the next stage's carry-in and
|
|
5065
5072
|
# `consumers.backfill_done_from_carry` glob them without knowing the
|
|
5066
5073
|
# producing run's layout. `RunRef.carry()` owns that flat-vs-staged rule;
|