okstra 0.167.0 → 0.169.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +6 -5
  2. package/docs/architecture/storage-model.md +57 -1
  3. package/docs/architecture.md +70 -2
  4. package/docs/cli.md +8 -4
  5. package/docs/for-ai/skills/okstra-code-review.md +3 -2
  6. package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
  7. package/docs/pr-template-usage.md +10 -6
  8. package/docs/project-structure-overview.md +14 -11
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/claude-worker.md +6 -5
  12. package/runtime/agents/workers/report-writer-worker.md +9 -4
  13. package/runtime/agents/workers/translator-worker.md +6 -4
  14. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
  15. package/runtime/prompts/coding-preflight/clean-code.md +13 -0
  16. package/runtime/prompts/duties/acceptance-critic.md +24 -0
  17. package/runtime/prompts/duties/acceptance-verifier.md +24 -0
  18. package/runtime/prompts/duties/analysis-worker.md +24 -0
  19. package/runtime/prompts/duties/code-reviewer.md +24 -0
  20. package/runtime/prompts/duties/common.md +35 -0
  21. package/runtime/prompts/duties/implementation-executor.md +24 -0
  22. package/runtime/prompts/duties/implementation-verifier.md +24 -0
  23. package/runtime/prompts/duties/lead.md +24 -0
  24. package/runtime/prompts/duties/report-writer.md +24 -0
  25. package/runtime/prompts/duties/reverification-worker.md +24 -0
  26. package/runtime/prompts/duties/schedule-verifier.md +24 -0
  27. package/runtime/prompts/duties/scope-critic.md +24 -0
  28. package/runtime/prompts/duties/translator.md +24 -0
  29. package/runtime/prompts/lead/convergence.md +51 -7
  30. package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
  31. package/runtime/prompts/lead/plan-body-verification.md +16 -1
  32. package/runtime/prompts/lead/report-writer.md +20 -5
  33. package/runtime/prompts/lead/team-contract.md +13 -13
  34. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  35. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  36. package/runtime/prompts/profiles/_implementation-executor.md +1 -1
  37. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  38. package/runtime/prompts/profiles/implementation.md +4 -2
  39. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
  40. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
  41. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
  42. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
  43. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
  44. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
  45. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
  46. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
  47. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
  48. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
  49. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
  50. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
  51. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
  52. package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
  53. package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
  54. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
  55. package/runtime/python/okstra_ctl/context_cost.py +46 -5
  56. package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
  57. package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
  58. package/runtime/python/okstra_ctl/doctor.py +150 -16
  59. package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
  60. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
  61. package/runtime/python/okstra_ctl/path_hints.py +26 -0
  62. package/runtime/python/okstra_ctl/paths.py +20 -0
  63. package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
  64. package/runtime/python/okstra_ctl/ports/host.py +3 -0
  65. package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
  66. package/runtime/python/okstra_ctl/pr_template.py +3 -6
  67. package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
  68. package/runtime/python/okstra_ctl/render.py +217 -12
  69. package/runtime/python/okstra_ctl/report_finalize.py +44 -0
  70. package/runtime/python/okstra_ctl/run.py +368 -51
  71. package/runtime/python/okstra_ctl/session.py +16 -12
  72. package/runtime/python/okstra_ctl/team.py +11 -11
  73. package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
  74. package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
  75. package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
  76. package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
  77. package/runtime/skills/okstra-code-review/SKILL.md +22 -3
  78. package/runtime/skills/okstra-run/SKILL.md +16 -1
  79. package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
  80. package/runtime/templates/implementation-worker-preamble.md +0 -10
  81. package/runtime/templates/report-writer-prompt-preamble.md +0 -9
  82. package/runtime/templates/reports/settings.template.json +0 -11
  83. package/runtime/templates/worker-prompt-preamble.md +0 -10
  84. package/runtime/validators/lib/fixtures.sh +93 -0
  85. package/runtime/validators/lib/validate-assets.sh +0 -8
  86. package/runtime/validators/validate-run.py +182 -0
  87. package/src/cli-registry.mjs +14 -0
  88. package/src/commands/execute/agent-prompt.mjs +25 -0
  89. package/src/commands/execute/codex-dispatch.mjs +6 -63
  90. package/src/commands/execute/worker-dispatch.mjs +76 -0
  91. package/src/commands/lifecycle/doctor.mjs +18 -3
  92. package/src/commands/lifecycle/install.mjs +33 -15
  93. package/src/commands/lifecycle/uninstall.mjs +4 -3
  94. package/src/lib/install-assets.mjs +9 -0
  95. package/runtime/agents/workers/antigravity-worker.md +0 -259
  96. package/runtime/agents/workers/codex-worker.md +0 -259
  97. package/runtime/agents/workers/grok-worker.md +0 -259
  98. package/runtime/agents/workers/kimi-worker.md +0 -259
  99. package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
  100. package/runtime/templates/operating-standard.md +0 -22
  101. package/src/lib/worker-agent-render.mjs +0 -50
  102. /package/runtime/templates/{prd → pr}/pr-body.template.md +0 -0
@@ -0,0 +1,104 @@
1
+ """Provider-neutral dispatcher for verified CLI-backed worker assignments."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any, Mapping, Sequence
9
+
10
+ from . import dispatch_core
11
+ from .adapters.dispatch.cli_wrapper import CliWrapperDispatchPort
12
+ from .application.dispatch_assignments import dispatch_assignments
13
+ from .dispatch_state import DispatchError
14
+ from .models import provider_wrappers
15
+ from .ports.worker_dispatch import WorkerDispatchRequest
16
+
17
+
18
+ SUPPORTED_CLI_WORKERS = provider_wrappers("analyser")
19
+
20
+
21
+ def main(argv: Sequence[str] | None = None) -> int:
22
+ args = _parser().parse_args(argv)
23
+ try:
24
+ if args.jobs_file and args.workers:
25
+ raise DispatchError("--jobs-file and --workers cannot be combined")
26
+ request = _dispatch_request(args)
27
+ port = CliWrapperDispatchPort(
28
+ supported_worker_wrappers=SUPPORTED_CLI_WORKERS,
29
+ unsupported_worker_label="worker-dispatch",
30
+ required_lead_runtime=None,
31
+ cli_wrapper_assignments_only=True,
32
+ default_provider_by_worker_id={"report-writer": "claude"},
33
+ )
34
+ plan = dispatch_assignments(request, port)
35
+ backend_plan = plan.backend_plan
36
+ if backend_plan is None:
37
+ raise DispatchError("CLI wrapper dispatch did not produce a backend plan")
38
+ if args.dry_run:
39
+ _print_json(_backend_payload(backend_plan, dry_run=True))
40
+ return 0
41
+ code = dispatch_core.dispatch_cli_wrapper_plan(backend_plan)
42
+ _print_json({**_backend_payload(backend_plan, dry_run=False), "exitCode": code})
43
+ return code
44
+ except DispatchError as exc:
45
+ print(f"error: {exc}", file=sys.stderr)
46
+ return 2
47
+
48
+
49
+ def _dispatch_request(args: argparse.Namespace) -> WorkerDispatchRequest:
50
+ return WorkerDispatchRequest(
51
+ project_root=Path(args.project_root),
52
+ run_manifest=Path(args.run_manifest),
53
+ workspace_root=Path(args.workspace_root),
54
+ okstra_bin=Path(args.okstra_bin) if args.okstra_bin else None,
55
+ requested_workers=tuple(_parse_workers(args.workers)),
56
+ idle_timeout_seconds=_parse_idle_timeout(args.idle_timeout_seconds),
57
+ dispatch_kind=args.dispatch_kind,
58
+ jobs_file=Path(args.jobs_file) if args.jobs_file else None,
59
+ )
60
+
61
+
62
+ def _backend_payload(backend_plan, *, dry_run: bool) -> dict[str, Any]:
63
+ payload = backend_plan.to_payload(dry_run=dry_run)
64
+ payload.pop("workerDispatches", None)
65
+ return payload
66
+
67
+
68
+ def _parser() -> argparse.ArgumentParser:
69
+ parser = argparse.ArgumentParser(
70
+ prog="okstra worker-dispatch",
71
+ description="Dispatch verified CLI-backed workers for a prepared run.",
72
+ )
73
+ parser.add_argument("--project-root", required=True)
74
+ parser.add_argument("--run-manifest", required=True)
75
+ parser.add_argument("--workspace-root", required=True)
76
+ parser.add_argument("--okstra-bin", default="")
77
+ parser.add_argument("--workers", default="")
78
+ parser.add_argument("--dry-run", action="store_true")
79
+ parser.add_argument("--idle-timeout-seconds", default="600")
80
+ parser.add_argument("--dispatch-kind", default="initial")
81
+ parser.add_argument("--jobs-file", default="")
82
+ return parser
83
+
84
+
85
+ def _parse_workers(raw: str) -> list[str]:
86
+ return [item.strip() for item in raw.split(",") if item.strip()]
87
+
88
+
89
+ def _parse_idle_timeout(raw: str) -> int:
90
+ try:
91
+ value = int(raw)
92
+ except ValueError as exc:
93
+ raise DispatchError("--idle-timeout-seconds must be an integer") from exc
94
+ if value < 0:
95
+ raise DispatchError("--idle-timeout-seconds must be non-negative")
96
+ return value
97
+
98
+
99
+ def _print_json(payload: Mapping[str, Any]) -> None:
100
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
101
+
102
+
103
+ if __name__ == "__main__":
104
+ raise SystemExit(main(sys.argv[1:]))
@@ -11,42 +11,25 @@ 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
- }
29
-
30
-
31
14
  def analysis_prompt_body(
32
15
  manifest: Mapping[str, Any],
33
16
  active_context: Mapping[str, Any],
34
17
  worker_id: str,
35
18
  model: str,
36
- role: str,
37
19
  plan: PromptPlan,
38
20
  ) -> list[str]:
39
21
  """Render the provider-neutral body for an initial analysis audience."""
40
22
  label = ANALYSIS_WORKER_LABELS.get(worker_id, f"{worker_id} worker")
23
+ pane_role = {
24
+ "implementation-executor": "executor",
25
+ "implementation-verifier": "verifier",
26
+ }.get(plan.audience, "worker")
41
27
  return [
42
28
  f"**Model:** {label}, {model}",
43
- f"**Pane role:** {role}",
29
+ f"**Pane role:** {pane_role}",
44
30
  "",
45
31
  f"# {label} Dispatch",
46
32
  "",
47
- "## Role",
48
- _role_statement(label, role),
49
- "",
50
33
  "## Task",
51
34
  f"- Task key: `{_require_string(manifest, 'taskKey')}`",
52
35
  f"- Task type: `{_require_string(manifest, 'taskType')}`",
@@ -62,16 +45,6 @@ def analysis_prompt_body(
62
45
  "- Write the audit sidecar to Audit sidecar path as the preamble requires.",
63
46
  "- Cite evidence with file paths and line numbers whenever you make a claim.",
64
47
  ]
65
-
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
-
75
48
  def analysis_input_lines(
76
49
  manifest: Mapping[str, Any],
77
50
  active_context: Mapping[str, Any],
@@ -108,12 +81,6 @@ def report_writer_prompt_body(
108
81
  "",
109
82
  "# Report Writer Worker Dispatch",
110
83
  "",
111
- "## Role",
112
- (
113
- "You are the Report writer worker. Author the final-report data.json, "
114
- "its rendered Markdown sibling, and the worker-result pointer."
115
- ),
116
- "",
117
84
  "## Task",
118
85
  f"- Task key: `{_require_string(manifest, 'taskKey')}`",
119
86
  f"- Task type: `{_require_string(manifest, 'taskType')}`",
@@ -1,6 +1,7 @@
1
1
  """Compact initial final-verification prompt contract."""
2
2
  from __future__ import annotations
3
3
 
4
+ import json
4
5
  import re
5
6
  from dataclasses import dataclass
6
7
  from pathlib import Path
@@ -81,6 +82,11 @@ _WORKER_SPECIFIC_PREFIXES = (
81
82
  "**File write mode:**",
82
83
  "**Model:**",
83
84
  "**Pane role:**",
85
+ "**Provider:**",
86
+ "**Model execution value:**",
87
+ "**Runner:**",
88
+ "**Host runtime:**",
89
+ "**Host model value:**",
84
90
  )
85
91
  _WORKER_LABEL_RE = re.compile(
86
92
  r"\b(?:Claude|Codex|Antigravity) worker\b",
@@ -95,6 +101,8 @@ class PromptRecord:
95
101
  path: Path
96
102
  expected_model: str | None = None
97
103
  expected_delivery_mode: str | None = None
104
+ metadata_path: Path | None = None
105
+ expected_duty_audience: str | None = None
98
106
 
99
107
 
100
108
  def validate_final_verification_initial_prompt(text: str) -> list[str]:
@@ -305,7 +313,8 @@ def _validate_model_header(text: str, expected_model: str | None) -> list[str]:
305
313
  """
306
314
  if expected_model is None:
307
315
  return []
308
- model = _model_value(_header_values(text, MODEL_HEADER))
316
+ task_text = text.split("\n\n## Task Instructions\n\n", 1)[-1]
317
+ model = _model_value(_header_values(task_text, MODEL_HEADER))
309
318
  if model is None:
310
319
  return ["exactly one non-empty **Model:** <label>, <model> header is required"]
311
320
  if model != expected_model:
@@ -314,13 +323,34 @@ def _validate_model_header(text: str, expected_model: str | None) -> list[str]:
314
323
 
315
324
 
316
325
  def _validate_record_metadata(text: str, record: PromptRecord) -> list[str]:
317
- return [
326
+ errors = [
318
327
  *_validate_delivery_mode(
319
328
  _header_values(text, PROMPT_DELIVERY_MODE_HEADER),
320
329
  record.expected_delivery_mode,
321
330
  ),
322
331
  *_validate_model_header(text, record.expected_model),
323
332
  ]
333
+ if record.expected_duty_audience is None:
334
+ return errors
335
+ if record.metadata_path is None:
336
+ return [*errors, "agent invocation metadata path is required"]
337
+ try:
338
+ metadata = json.loads(
339
+ record.metadata_path.read_text(encoding="utf-8")
340
+ )
341
+ except (OSError, UnicodeError, ValueError):
342
+ return [*errors, "agent invocation metadata is invalid"]
343
+ duty = metadata.get("dutyContract") if isinstance(metadata, dict) else None
344
+ if (
345
+ not isinstance(duty, dict)
346
+ or metadata.get("audience") != record.expected_duty_audience
347
+ or duty.get("id") != record.expected_duty_audience
348
+ ):
349
+ errors.append(
350
+ "agent invocation duty does not match expected audience: "
351
+ + record.expected_duty_audience
352
+ )
353
+ return errors
324
354
 
325
355
 
326
356
  def _validate_evidence_ledger_header(
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  from dataclasses import dataclass
5
5
  from typing import Any, Literal, Mapping
6
6
 
7
+ from .agent_invocation import AgentAudience
7
8
  from .analysis_inputs import ANALYSIS_TASK_TYPES
8
9
 
9
10
 
@@ -12,6 +13,7 @@ PromptAudience = Literal[
12
13
  "implementation-executor",
13
14
  "implementation-verifier",
14
15
  "report-writer",
16
+ "translator",
15
17
  "reverify",
16
18
  "lead-only",
17
19
  ]
@@ -68,6 +70,7 @@ WORKER_ERROR_CONTRACT_FILENAME = "worker-error-contract.md"
68
70
  @dataclass(frozen=True)
69
71
  class PromptPlan:
70
72
  audience: PromptAudience
73
+ duty_audience: AgentAudience
71
74
  equality_group: str | None
72
75
  packet_only: bool
73
76
  allow_coding_preflight: bool
@@ -90,6 +93,10 @@ def resolve_prompt_plan(
90
93
  return _plan("lead-only")
91
94
  if not worker_id.strip():
92
95
  raise ValueError("worker ID is required")
96
+ if worker_id == "translator" or dispatch_kind == "translator":
97
+ if worker_id != "translator" or dispatch_kind != "translator":
98
+ raise ValueError("translator worker and dispatch kind must match")
99
+ return _plan("translator", duty_audience="translator")
93
100
  if dispatch_kind.startswith("reverify-r"):
94
101
  return _plan("reverify")
95
102
  # A critic pass keeps the full analysis contract — worker anchor headers, the
@@ -99,6 +106,15 @@ def resolve_prompt_plan(
99
106
  # analysis prompts makes `validate_analysis_prompt_set` compare two prompts
100
107
  # that are *supposed* to differ and fail every critic-enabled run.
101
108
  analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
109
+ analysis_duty: AgentAudience = (
110
+ "acceptance-critic"
111
+ if dispatch_kind == "critic" and task_type == "final-verification"
112
+ else "scope-critic"
113
+ if dispatch_kind == "critic"
114
+ else "acceptance-verifier"
115
+ if task_type == "final-verification"
116
+ else "analysis-worker"
117
+ )
102
118
  if task_type == "implementation" and not executor_worker_id:
103
119
  raise ValueError("implementation executor worker ID is required")
104
120
  if worker_id == "report-writer":
@@ -119,6 +135,7 @@ def resolve_prompt_plan(
119
135
  if task_type == "final-verification":
120
136
  return _plan(
121
137
  "analysis",
138
+ duty_audience=analysis_duty,
122
139
  equality_group=analysis_equality_group,
123
140
  packet_only=True,
124
141
  required_headers=FINAL_VERIFICATION_HEADERS,
@@ -128,11 +145,17 @@ def resolve_prompt_plan(
128
145
  if task_type == "improvement-discovery":
129
146
  return _plan(
130
147
  "analysis",
148
+ duty_audience=analysis_duty,
131
149
  equality_group=analysis_equality_group,
132
150
  packet_only=True,
133
151
  required_headers=(GRILLING_LOG_HEADER,),
134
152
  )
135
- return _plan("analysis", equality_group=analysis_equality_group, packet_only=True)
153
+ return _plan(
154
+ "analysis",
155
+ duty_audience=analysis_duty,
156
+ equality_group=analysis_equality_group,
157
+ packet_only=True,
158
+ )
136
159
 
137
160
 
138
161
  def resolve_prompt_plan_for_manifest(
@@ -154,6 +177,7 @@ def resolve_prompt_plan_for_manifest(
154
177
  def _plan(
155
178
  audience: PromptAudience,
156
179
  *,
180
+ duty_audience: AgentAudience | None = None,
157
181
  equality_group: str | None = None,
158
182
  packet_only: bool = False,
159
183
  allow_coding_preflight: bool = False,
@@ -163,6 +187,7 @@ def _plan(
163
187
  ) -> PromptPlan:
164
188
  return PromptPlan(
165
189
  audience=audience,
190
+ duty_audience=duty_audience or _duty_audience(audience),
166
191
  equality_group=equality_group,
167
192
  packet_only=packet_only,
168
193
  allow_coding_preflight=allow_coding_preflight,
@@ -172,6 +197,18 @@ def _plan(
172
197
  )
173
198
 
174
199
 
200
+ def _duty_audience(audience: PromptAudience) -> AgentAudience:
201
+ return {
202
+ "analysis": "analysis-worker",
203
+ "implementation-executor": "implementation-executor",
204
+ "implementation-verifier": "implementation-verifier",
205
+ "report-writer": "report-writer",
206
+ "translator": "translator",
207
+ "reverify": "reverification-worker",
208
+ "lead-only": "lead",
209
+ }[audience]
210
+
211
+
175
212
  def _worker_facing_headers(
176
213
  audience: PromptAudience,
177
214
  required_headers: tuple[str, ...],
@@ -107,9 +107,19 @@ okstra code-review target --branch <name> [--base <ref>] --project-root <project
107
107
 
108
108
  A large census is never truncated. Report the cell count and confirm before dispatching — a silent cut is a false "I looked at everything" signal.
109
109
 
110
- ## Step 3 — Dispatch four reviewers in parallel
110
+ ## Step 3 — Materialize and dispatch four reviewers in parallel
111
111
 
112
- Send all four Agent calls in **one message**, `subagent_type: "general-purpose"`. Every brief carries:
112
+ Every reviewer and later gap-fill is a separate auditable standalone invocation. Before dispatch, create
113
+ `.okstra/agent-invocations/code-review/<invocation-id>.instructions.md` from that reviewer's brief, then run
114
+ `okstra agent-prompt materialize` with `--purpose code-review`, `--audience code-reviewer`, and the canonical
115
+ `.prompt.md` path beside it. Pass the current host runtime, selected provider, and `--model-role analyser`;
116
+ the returned assignment is authoritative. Run `okstra agent-prompt verify` against the returned
117
+ `metadataPath` before invoking any model.
118
+
119
+ For a native host call, pass the verified prompt body and `hostModelValue`. For a deterministic provider
120
+ process, run `okstra worker-dispatch` with the verified prompt path and `modelExecutionValue`; never
121
+ substitute one model value for the other. Dispatch the four verified calls in parallel when the host supports
122
+ it. Every brief carries:
113
123
 
114
124
  - the diff, plus the work directory path so the reviewer can read whole files for context
115
125
  - the project layout in one or two lines (where source, tests, and — if the routing found one — domain / ports / adapters live)
@@ -130,9 +140,18 @@ Axis scope — the `Reads` column names that group's rules, and a brief never re
130
140
 
131
141
  Say it plainly in every brief: **the census is law** — work the cells exactly as given, return a verdict for every one, never re-derive the list. There is no length budget; dropping a finding to stay brief is the failure this skill exists to prevent.
132
142
 
143
+ Capture each raw model return under `.okstra/agent-invocations/code-review/.tmp/`. Publish it with
144
+ `okstra agent-prompt materialize-result`, publish the immutable completion marker with
145
+ `okstra agent-prompt complete`, then run `okstra agent-prompt verify-completion`. Parse only the
146
+ `returnedBody` emitted by that last command. A model response without a verified completion is not a
147
+ review result and cannot contribute a verdict.
148
+
133
149
  ## Step 3.5 — Audit the coverage
134
150
 
135
- Diff each reviewer's returned cells against the slice you handed it. Any cell without a verdict → re-dispatch **one gap-fill agent per axis**, carrying only the missing cells and the same brief. Repeat until every cell of every axis has a verdict.
151
+ Diff each reviewer's verified `returnedBody` cells against the slice you handed it. Any cell without a verdict
152
+ → dispatch **one gap-fill invocation per axis**, carrying only the missing cells and the same brief. Each
153
+ gap-fill uses a new invocation ID and repeats the full materialize → verify → dispatch → materialize-result →
154
+ complete → verify-completion boundary from Step 3. Repeat until every cell of every axis has a verdict.
136
155
 
137
156
  A missing verdict is unfinished work, never an implicit `clean`. Do not start Step 4 while a single cell is unaccounted for.
138
157
 
@@ -258,7 +258,22 @@ okstra render-bundle \
258
258
  --<each-remaining-renderArgs-key> "<corresponding-value>"
259
259
  ```
260
260
 
261
- `render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full rendered lead prompt. Parse the labelled lines for `TASK_ROOT` and `INSTRUCTION_SET_PATH`. Also watch for an optional `okstra concurrent-run stages:` label line — present only when a concurrent run is detected (see "Concurrent-run detection branch" below).
261
+ `render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, `okstra run manifest:`, and the full rendered lead prompt. Parse the labelled lines for `TASK_ROOT`, `INSTRUCTION_SET_PATH`, and `RUN_MANIFEST_PATH`. Also watch for an optional `okstra concurrent-run stages:` label line — present only when a concurrent run is detected (see "Concurrent-run detection branch" below).
262
+
263
+ Before acting as the lead, read `resources.leadPromptMetadataPath` from the run
264
+ manifest and execute the host-native specification-link gate:
265
+
266
+ ```bash
267
+ okstra agent-prompt record-dispatch \
268
+ --project-root <projectRoot> \
269
+ --run-manifest <RUN_MANIFEST_PATH> \
270
+ --metadata <projectRoot/resources.leadPromptMetadataPath> \
271
+ --enforcement-mode host-native-spec-link-gate
272
+ ```
273
+
274
+ Do not continue from the rendered prompt if this command fails. This record
275
+ associates the current-session lead with a verified invocation specification;
276
+ it does not claim that the host exposed or attested the delivered prompt bytes.
262
277
 
263
278
  The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.lock`), writes `run-context-*.json` + `run-inputs-*.json` + all manifests + discovery files, and registers the run in `~/.okstra/recent.jsonl` with status `prepared`.
264
279
 
@@ -221,7 +221,16 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart skip
221
221
  ```
222
222
 
223
223
  3. **Run the deterministic gate first.** Execute `python3 ~/.okstra/lib/validators/validate-schedule.py <draft> --selection-json <selection>`. Do not dispatch the narrative verifier when this exits non-zero.
224
- 4. **Run the independent LLM verifier second.** Only after the deterministic gate passes, dispatch an independent verifier subagent with the draft and selection JSON, but without the lead's reasoning. It returns `pass` plus concrete findings. Give it these checks:
224
+ 4. **Run the independent LLM verifier second.** Only after the deterministic gate passes, create
225
+ `.okstra/agent-invocations/schedule-verification/<invocation-id>.instructions.md` containing the draft,
226
+ selection JSON, and checks below, but none of the lead's reasoning. Materialize its sibling `.prompt.md`
227
+ with `okstra agent-prompt materialize`, `--purpose schedule-verification`,
228
+ `--audience schedule-verifier`, the current host runtime, the selected provider, and
229
+ `--model-role analyser`. Run `okstra agent-prompt verify` against the returned `metadataPath` before
230
+ dispatch. A native host call receives the verified prompt body and `hostModelValue`; a deterministic
231
+ provider process runs `okstra worker-dispatch` with the prompt path and `modelExecutionValue`. These
232
+ values are not interchangeable.
233
+ The verifier returns `pass` plus concrete findings. Give it these checks:
225
234
  - **Executable ordering** — stage sequence, `Depends On`, and Gantt bar positions tell the same story; nothing depends on something scheduled after it.
226
235
  - **Arithmetic** — the Work Breakdown `Days` column sums to the At a Glance `Days` cell and to the `Effort sum` line.
227
236
  - **Engineering-only scope** — no approval gate, permission check, stakeholder alignment or decision checklist.
@@ -231,6 +240,11 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart skip
231
240
  - **One language.** Table cells and stage titles follow the schedule's prose language; only headings and field labels stay English literals.
232
241
 
233
242
  These last three are the checks no validator can make. Do not accept a `pass` that skipped them.
243
+
244
+ Capture the raw return under `.okstra/agent-invocations/schedule-verification/.tmp/`, then run
245
+ `okstra agent-prompt materialize-result`, `okstra agent-prompt complete`, and
246
+ `okstra agent-prompt verify-completion` in that order. Parse only the `returnedBody` emitted by
247
+ `verify-completion`; an inline or unverified response cannot pass this gate.
234
248
  5. **Revise from the first gate after every change.** If either gate requests a change, the lead revises the same draft in place, then starts again at the deterministic `--selection-json` gate. Allow **Max 2 revise cycles** total across both gates.
235
249
  6. **Gate publication.** Only the same draft that passes both gates may be promoted in Step 6. If it still fails after the second revision, do not write the final file; remove the paired staging artifacts and report the residual findings in Korean.
236
250
 
@@ -2,16 +2,6 @@
2
2
 
3
3
  This file is shared by the `implementation-executor` and `implementation-verifier` audiences. Read it end-to-end, then read the role sidecar enumerated by the prompt. Before work, also read the shared file named by `**Worker Error Contract Path:**`; error rules live only there.
4
4
 
5
- ## Operating standard (read before any output)
6
-
7
- Work like a senior engineer who owns this result, not a commentator on it.
8
- - Evidence over assertion — back every claim with a file:line, or mark it an explicit assumption. Never state the unverified as fact.
9
- - Read before you reason — read each required input end to end; when you lack basis, write "insufficient evidence" instead of filling the gap plausibly.
10
- - Shortest sound path — chase the most likely cause first; don't re-verify what is settled or pad with restatement.
11
- - Decide, don't survey — when options exist, give the trade-off and one recommendation, not an exhaustive list.
12
- - Fit what's here — match the surrounding code and prose; size the response to the request.
13
- - Hold your own line — reason from your independent angle; do not drift toward the other workers' likely answers or echo the user. Triangulation fails if your view isn't genuinely yours.
14
-
15
5
  ## Required reading
16
6
 
17
7
  - Read the implementation role sidecar named by the prompt end-to-end. The executor sidecar owns mutation behavior; the verifier sidecar owns read-only review behavior.
@@ -2,15 +2,6 @@
2
2
 
3
3
  This file is the audience-specific contract for `report-writer`. Read it end-to-end. Before work, also read the shared file named by `**Worker Error Contract Path:**`; error rules live only there.
4
4
 
5
- ## Operating standard (read before any output)
6
-
7
- Work like a senior engineer who owns this result, not a commentator on it.
8
- - Evidence over assertion — back every claim with a file:line, or mark it an explicit assumption. Never state the unverified as fact.
9
- - Read before you reason — read each required input end to end; when you lack basis, write "insufficient evidence" instead of filling the gap plausibly.
10
- - Shortest sound path — chase the most likely cause first; don't re-verify what is settled or pad with restatement.
11
- - Decide, don't survey — when options exist, give the trade-off and one recommendation, not an exhaustive list.
12
- - Fit what's here — match the surrounding code and prose; size the response to the request.
13
-
14
5
  ## Required reading
15
6
 
16
7
  Read every input enumerated by the Phase 6 dispatch end-to-end: task/analysis inputs, worker results, convergence state, the instruction-set-local `final-report-template.md`, and the task-type excerpt `final-report-schema.json`. Do not pull the full repository template or schema when the scoped instruction-set copies are provided.
@@ -49,17 +49,6 @@
49
49
  ]
50
50
  }
51
51
  ],
52
- "PreToolUse": [
53
- {
54
- "matcher": "Write|Edit|MultiEdit|NotebookEdit",
55
- "hooks": [
56
- {
57
- "type": "command",
58
- "command": "bash \"$HOME/.okstra/prompts/coding-preflight/scripts/preedit-check.sh\""
59
- }
60
- ]
61
- }
62
- ],
63
52
  "SessionEnd": [
64
53
  {
65
54
  "hooks": [
@@ -2,16 +2,6 @@
2
2
 
3
3
  This file is the audience-specific contract for initial `analysis` workers. Read it end-to-end from the path selected by the dispatch prompt. Before doing work, also read the shared file named by `**Worker Error Contract Path:**`; that file is the only source for error-sidecar rules.
4
4
 
5
- ## Operating standard (read before any output)
6
-
7
- Work like a senior engineer who owns this result, not a commentator on it.
8
- - Evidence over assertion — back every claim with a file:line, or mark it an explicit assumption. Never state the unverified as fact.
9
- - Read before you reason — read each required input end to end; when you lack basis, write "insufficient evidence" instead of filling the gap plausibly.
10
- - Shortest sound path — chase the most likely cause first; don't re-verify what is settled or pad with restatement.
11
- - Decide, don't survey — when options exist, give the trade-off and one recommendation, not an exhaustive list.
12
- - Fit what's here — match the surrounding code and prose; size the response to the request.
13
- - Hold your own line — reason from your independent angle; do not drift toward the other workers' likely answers or echo the user. Triangulation fails if your view isn't genuinely yours.
14
-
15
5
  ## Required reading
16
6
 
17
7
  Read `analysis-packet.md`, the primary compact input, end-to-end. Source files named as fallback/evidence paths are read only when a finding needs to verify a citation, fill a packet gap, or resolve ambiguity. Do not read `final-report-template.md` or `final-report-schema.json`; analysis workers produce findings, not the final report.
@@ -526,5 +526,98 @@ if lead_sid:
526
526
  write_json(team_state_path, team_state)
527
527
  write_json(run_manifest_path, run_manifest)
528
528
  write_json(task_manifest_path, task_manifest)
529
+
530
+ # The synthetic fixture accepts lead and completed-worker results, so it must
531
+ # create the same verified invocation records and result links as a real run.
532
+ # Keep the deliberately omitted worker prompt absent: the first validator pass
533
+ # still exercises the historical prompt-history failure before the helper
534
+ # restores that file.
535
+ if WORKSPACE_ROOT:
536
+ from okstra_ctl.agent_invocation import (
537
+ AgentInstruction,
538
+ AgentInstructionSource,
539
+ AgentInvocationRequest,
540
+ agent_model_assignment_from_payload,
541
+ prepare_agent_invocation,
542
+ )
543
+ from okstra_ctl.dispatch_state import (
544
+ link_agent_dispatch_result,
545
+ record_verified_agent_dispatch,
546
+ )
547
+
548
+ lead_record = record_verified_agent_dispatch(
549
+ project_root=project_root,
550
+ run_manifest_path=run_manifest_path,
551
+ metadata_path=project_root / run_manifest["leadPromptMetadataPath"],
552
+ enforcement_mode="host-native-spec-link-gate",
553
+ )
554
+ link_agent_dispatch_result(
555
+ project_root=project_root,
556
+ run_manifest_path=run_manifest_path,
557
+ dispatch_id=lead_record["dispatchId"],
558
+ result_path=report_path,
559
+ )
560
+
561
+ assignments = run_manifest["invocationAssignments"]
562
+ contract = run_manifest["agentContract"]
563
+ reservation_root = project_root / contract["invocationReservationRootPath"]
564
+ for worker in team_state.get("workers", []):
565
+ if not isinstance(worker, dict) or worker.get("status") != "completed":
566
+ continue
567
+ worker_id = str(worker.get("workerId") or "").strip()
568
+ prompt_relative = str(worker.get("promptPath") or "").strip()
569
+ result_relative = str(worker.get("resultPath") or "").strip()
570
+ if not worker_id or not prompt_relative or not result_relative:
571
+ continue
572
+ source_prompt = project_root / prompt_relative
573
+ if not source_prompt.is_file():
574
+ continue
575
+ assignment_ref = f"initial/{worker_id}"
576
+ invocation_id = f"validation-fixture-{worker_id}"
577
+ invocation_prompt = reservation_root / f"{invocation_id}.prompt.md"
578
+ prepared = prepare_agent_invocation(AgentInvocationRequest(
579
+ invocation_id=invocation_id,
580
+ worker_id=worker_id,
581
+ audience=(
582
+ "report-writer" if worker_id == "report-writer"
583
+ else "analysis-worker"
584
+ ),
585
+ assignment_ref=assignment_ref,
586
+ purpose=None,
587
+ assignment=agent_model_assignment_from_payload(
588
+ assignments[assignment_ref]
589
+ ),
590
+ instruction=AgentInstruction(
591
+ anchor_lines=(),
592
+ body=source_prompt.read_text(encoding="utf-8"),
593
+ source_paths=(
594
+ AgentInstructionSource("project", prompt_relative),
595
+ ),
596
+ ),
597
+ project_root=project_root,
598
+ run_manifest_path=run_manifest_path,
599
+ duty_root=project_root / contract["dutyRootPath"],
600
+ prompt_path=invocation_prompt,
601
+ metadata_path=invocation_prompt.with_name(
602
+ invocation_prompt.name + ".meta.json"
603
+ ),
604
+ dispatch_kind="validation-fixture",
605
+ ))
606
+ dispatch = record_verified_agent_dispatch(
607
+ project_root=project_root,
608
+ run_manifest_path=run_manifest_path,
609
+ metadata_path=prepared.metadata_path,
610
+ enforcement_mode=(
611
+ "host-native-spec-link-gate"
612
+ if prepared.assignment.runner == "native-session"
613
+ else "core-pre-dispatch"
614
+ ),
615
+ )
616
+ link_agent_dispatch_result(
617
+ project_root=project_root,
618
+ run_manifest_path=run_manifest_path,
619
+ dispatch_id=dispatch["dispatchId"],
620
+ result_path=project_root / result_relative,
621
+ )
529
622
  PY
530
623
  }
@@ -41,20 +41,12 @@ def check(source_path: Path, target_path: Path) -> None:
41
41
 
42
42
  # 1. Worker agent files: agents/workers/*-worker.md -> runtime/agents/workers/*-worker.md
43
43
  #
44
- # `_cli-wrapper-template.md` is a build-time render INPUT, not a shipped
45
- # artifact — tools/build.mjs renders it (with each *.params.json) into
46
- # codex-worker.md / antigravity-worker.md and excludes the template itself from the
47
- # runtime payload. Skip it here so parity is checked only on shipped files.
48
- # Keep this set in sync with TEMPLATE_INPUT_BASENAMES in tools/build.mjs.
49
- template_input_basenames = {"_cli-wrapper-template.md"}
50
44
  workers_source = agents_source_root / "workers"
51
45
  workers_target = runtime_root / "agents" / "workers"
52
46
  if not workers_source.is_dir():
53
47
  errors.append(f"missing agents/workers source directory: {workers_source}")
54
48
  else:
55
49
  for source_path in sorted(workers_source.glob("*.md")):
56
- if source_path.name in template_input_basenames:
57
- continue
58
50
  check(source_path, workers_target / source_path.name)
59
51
 
60
52
  # 2. Lead contract + internal lead resources: prompts/lead/* + prompts/coding-preflight/*