okstra 0.133.0 → 0.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/architecture/storage-model.md +1 -1
- package/docs/cli.md +1 -1
- package/docs/for-ai/skills/okstra-pr-gen.md +2 -1
- package/docs/for-ai/skills/okstra-user-response.md +9 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +3 -1
- package/runtime/bin/okstra-antigravity-exec.sh +12 -2
- package/runtime/bin/okstra-claude-exec.sh +10 -1
- package/runtime/bin/okstra-codex-exec.sh +12 -2
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/team-contract.md +2 -2
- package/runtime/prompts/profiles/_common-contract.md +0 -1
- package/runtime/prompts/profiles/_implementation-executor.md +20 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/python/okstra_ctl/clarification_items.py +34 -12
- package/runtime/python/okstra_ctl/convergence_engine.py +11 -2
- package/runtime/python/okstra_ctl/models.py +2 -0
- package/runtime/python/okstra_ctl/render_final_report.py +1 -1
- package/runtime/python/okstra_ctl/run.py +30 -11
- package/runtime/python/okstra_ctl/session.py +36 -7
- package/runtime/python/okstra_ctl/user_response.py +14 -5
- package/runtime/python/okstra_ctl/worker_artifact_paths.py +5 -1
- package/runtime/python/okstra_ctl/worker_heartbeat.py +35 -4
- package/runtime/python/okstra_ctl/worker_liveness.py +19 -6
- package/runtime/python/okstra_ctl/worktree.py +39 -9
- package/runtime/python/okstra_token_usage/collect.py +75 -9
- package/runtime/python/okstra_token_usage/pricing.py +4 -1
- package/runtime/skills/okstra-pr-gen/SKILL.md +24 -0
- package/runtime/skills/okstra-user-response/SKILL.md +67 -19
- package/runtime/validators/forbidden_actions.py +1 -1
- package/runtime/validators/validate-run.py +26 -8
- package/runtime/validators/validate_session_conformance.py +17 -7
|
@@ -70,7 +70,7 @@ Artifacts from each execution accumulate beneath the current run's resolved `run
|
|
|
70
70
|
<target-project>/.okstra/tasks/<task-group>/<task-id>/runs/<task-type>/
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
The representative files below are all relative to the resolved run directory (`run_dir`). Therefore, report, log,
|
|
73
|
+
The representative files below are all relative to the resolved run directory (`run_dir`). Therefore, report, log, and session paths for stage-isolated runs include the `stage-<N>/` segment. The carry sidecar is the exception — it stays flat under the task-type run dir (see the `carry/stage-<N>.json` entry below).
|
|
74
74
|
|
|
75
75
|
- `manifests/run-manifest-<task-type>-<seq>.json`
|
|
76
76
|
- `state/team-state-<task-type>-<seq>.json`
|
package/docs/cli.md
CHANGED
|
@@ -343,7 +343,7 @@ Then `okstra codex-dispatch --project-root <dir> --run-manifest <run-manifest> -
|
|
|
343
343
|
The Codex worker (`--workers codex`, `--codex-model`) and Codex lead runtime are separate. The former selects a worker provider dispatched by a Claude Code lead; the latter is the upper runtime boundary for running the lead itself through the Codex adapter.
|
|
344
344
|
|
|
345
345
|
> Every `--*-model` flag accepts only aliases registered in the provider mappings in `scripts/okstra_ctl/models.py`. An unregistered value is immediately rejected with `UnknownModelError`, preventing a contract violation where the manifest's `modelExecutionValue` differs from the actual execution value. Allowed values:
|
|
346
|
-
> - Claude (`--lead-model` / `--claude-model` / `--report-writer-model`): `fable`, `fable-5`, `claude-fable-5`, `opus`, `opus-4-8`, `claude-opus-4-8`, `opus-4-7`, `claude-opus-4-7`, `opus-4-6`, `claude-opus-4-6`, `sonnet`, `sonnet-4-6`, `claude-sonnet-4-6`, `haiku`, `haiku-4-5`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`
|
|
346
|
+
> - Claude (`--lead-model` / `--claude-model` / `--report-writer-model`): `fable`, `fable-5`, `claude-fable-5`, `opus`, `opus-5`, `claude-opus-5`, `opus-4-8`, `claude-opus-4-8`, `opus-4-7`, `claude-opus-4-7`, `opus-4-6`, `claude-opus-4-6`, `sonnet`, `sonnet-4-6`, `claude-sonnet-4-6`, `haiku`, `haiku-4-5`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`
|
|
347
347
|
> - Codex (`--codex-model`): `gpt-5.6-sol`, `gpt-5.6`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `codex-auto-review`
|
|
348
348
|
> - Antigravity (`--antigravity-model`): `gemini-3.1-pro` (default), `gemini-3.5-flash`, and the space-separated aliases `gemini 3.1 pro` / `gemini 3.5 flash`. The antigravity worker uses the `agy` CLI to run Gemini-family models, so model IDs retain the `gemini-*` form.
|
|
349
349
|
|
|
@@ -34,7 +34,8 @@ A 3-option picker via `AskUserQuestion`:
|
|
|
34
34
|
1. Pick a template: `okstra pr template list --json`. If empty, use the bundled default. Carry the chosen name as `<template>` (`default` for the bundled one).
|
|
35
35
|
2. Pick the base branch: `okstra pr branches --json`. 3-option from the top `recommended` entries plus `Enter directly`. Carry the choice as `<base>`.
|
|
36
36
|
3. Generation bundle: `okstra pr gen --base <base> --template <template> --json` → `{base, currentBranch, templateName, template, commits, diffStat}`. Then **read the real diff honestly** (SSOT): `git diff <base>...HEAD` (large diffs section by section). Fill the placeholders from the diff and commits, describing **only actual changes**. Mark a checklist box `[x]` only when the diff supports it (tests touched → tests box, docs touched → docs box). If `commits`/`diffStat` are empty, say there is nothing to describe and stop. **Never append AI trailers/footers.**
|
|
37
|
-
4.
|
|
37
|
+
4. Identifier allowlist for the title and body: only repo-relative source paths (optionally `path:line`), symbol names present in the diff, branch names / commit subjects / SHAs, and issue-tracker ticket ids the reviewer can open. okstra's own artifact identifiers are out of the allowlist — report item ids (`F-001`, `C-001`, `R-001`, `D-0001`, `PREP-001`), run artifact names and their `<task-type>-<seq>` suffixes, phase/stage/worker labels (`final-verification`, `stage-2`, `codex-worker`), and any path under `.okstra/`. They resolve to nothing for a reviewer; restate the substance in code terms instead of citing the id.
|
|
38
|
+
5. Output and offer to create the PR: print the filled PR body as a single fenced markdown block. Ask whether to open a PR. **Only on an explicit yes**: write the body to a temp file and run `gh pr create --base <base> --title "<title>" --body-file <path>`. If `gh` is missing or unauthenticated (`gh auth status` fails), leave the text in chat and give manual-creation guidance. **No push/PR creation without the user's confirmation.**
|
|
38
39
|
|
|
39
40
|
## Mode B — Register template
|
|
40
41
|
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
`okstra-user-response` answers the **unresolved clarification questions** an okstra run left behind (the open `C-*` rows under the final report's `## 1. Clarification Items`) **in-session**, and records those answers as a `runs/<type>/user-responses/` sidecar. The next `/okstra-run` auto-attaches this sidecar via `--clarification-response`.
|
|
12
12
|
|
|
13
|
-
**Core principle — the skill never picks an answer for the user.** It
|
|
13
|
+
**Core principle — the skill never picks an answer for the user.** It builds the option board — background, a self-contained question, three concrete answers, `Enter directly` — and the user alone picks from it; every `value` is what the user chose or typed. It does not call `write` until the user has explicitly confirmed (`confirmed`).
|
|
14
|
+
|
|
15
|
+
**Second principle — one question at a time.** Never batch two clarification items into one question, and never dump the whole open list at the user.
|
|
14
16
|
|
|
15
17
|
Distinguish it from starting a run (`okstra-run`), inspecting a finished task (`okstra-inspect`), and generating a brief (`okstra-brief-gen`).
|
|
16
18
|
|
|
@@ -34,9 +36,12 @@ okstra preflight --runtime claude-code --json
|
|
|
34
36
|
|
|
35
37
|
## Flow
|
|
36
38
|
|
|
37
|
-
1. **list**: `okstra user-response list --home <home> --project <projectId> --limit 3` → an array of `{taskKey, taskType, seq, reportPath, reportMtime, openApprovalCount, unreadable}
|
|
38
|
-
2. **show**: `okstra user-response show --report <reportPath>` → `rows[]` with `resolvedRefs` (the `definition` of internal tokens such as `RB-002`/`§4.7`).
|
|
39
|
-
3. **
|
|
39
|
+
1. **list**: `okstra user-response list --home <home> --project <projectId> --limit 3` → an array of `{taskKey, taskType, seq, reportPath, reportMtime, openBlockerCount, openApprovalCount, unreadable}` (`openBlockerCount` = open rows with `Blocks` in `{approval, next-phase}`; `openApprovalCount` = the `approval`-only subset). If the array is empty, stop with "no open clarification". A 3-option picker (top recommendations + the final option always "Enter directly" for pasting a `reportPath`/`task-key` directly). `unreadable:true` is a §1 format drift — flag it with `⚠` and do not proceed (do not fabricate rows).
|
|
40
|
+
2. **show (data fetch, not a presentation step)**: `okstra user-response show --report <reportPath>` → `rows[]` with `resolvedRefs` (the `definition` of internal tokens such as `RB-002`/`§4.7`). Do not print `rows` at the user and **do not paste the raw `statement` as the question** — announce only `<N> open items — I'll go through them one at a time.`
|
|
41
|
+
3. **ask, one item at a time**: iterate the rows in report order, **one item per `AskUserQuestion` call**, headed `[n/N] C-014 — blocks: approval gate`. Per item:
|
|
42
|
+
- **Background first**, in the message text above the picker, 3–6 lines: (a) *Situation* — what the run was doing when it stopped here; (b) *What is undecided* — the fork, internal tokens expanded inline from `resolvedRefs[].definition`, plus what is stuck (`approval` → the approval gate stays shut and `implementation` cannot start; `next-phase` → the next phase cannot begin); (c) *What changes with your answer*. Source it from `resolvedRefs[].definition`, else **Read** the `§`/`path:line` in `contextRefs[]`; **never invent it** — say the report is silent instead. Close with `Source: C-014 — "<raw statement>"`.
|
|
43
|
+
- **Picker: exactly four options** — 1) `recommended` restated plainly, label suffixed `(Recommended)`, rationale in the description; 2) `alternatives[0]`; 3) `alternatives[1]`; 4) `Enter directly`, always last. When `alternatives[]` is short, fill the slot with a concrete candidate the report itself names (description opens `Not an okstra proposal — from <where>`), else `Reframe this question` — which is what guarantees three answers plus `Enter directly`. Never pad with an unsourced option; never mark anything but `recommended` as recommended.
|
|
44
|
+
- **Transcribe**: option 1–3 → `value` = that option's full answer text, `disposition:"answer"`; `Enter directly` → the user's utterance verbatim, `disposition:"answer"`; `Reframe this question` → `disposition:"reframe"` (does not satisfy the approval gate). A question back from the user records nothing — **Read** the ref, explain, re-ask the same item with the same four options. Echo `[n/N] C-014 → answer: …` and move on. Each item's JSON: `{id, kind, value, rationale?, disposition}`.
|
|
40
45
|
4. **echo → confirmed gate**: before `write`, echo the whole collection (each `id`·`disposition`·`value`·`rationale`·approval) as-is and get explicit confirmation. Never `write` before `confirmed`. On any change, re-echo and re-confirm.
|
|
41
46
|
5. **approval (optional)**: only when the approval-blocking items are **all filled with an answer** and the user explicitly approved, `--approval '{"approved":true,"implementationOption":"<selected option>"}'`. If any item is unfilled/reframe, do not approve and say the gate is still open.
|
|
42
47
|
6. **write**: `okstra user-response write --report <reportPath> --answers '<json>' [--approval '<json>'] --task-key <taskKey>` → report the returned `{sidecar:<path>}`. (When a same-named sidecar exists, the same `id` is overwritten with the new value and merged.)
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -91,7 +91,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
91
91
|
- Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-antigravity-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `ANTIGRAVITY_CLI_TIMEOUT: agy exec exceeded polling cap`.
|
|
92
92
|
4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
|
|
93
93
|
- Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
|
|
94
|
-
- **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
|
|
94
|
+
- **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s, or 1500s for the build-running `executor` / `verifier` roles), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
|
|
95
95
|
- **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
|
|
96
96
|
- Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
|
|
97
97
|
- **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
|
|
@@ -91,7 +91,7 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
91
91
|
- Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-codex-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `CODEX_CLI_TIMEOUT: codex exec exceeded polling cap`.
|
|
92
92
|
4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
|
|
93
93
|
- Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
|
|
94
|
-
- **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
|
|
94
|
+
- **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s, or 1500s for the build-running `executor` / `verifier` roles), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
|
|
95
95
|
- **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
|
|
96
96
|
- Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
|
|
97
97
|
- **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
|
|
@@ -41,7 +41,9 @@ Do NOT duplicate the data.json contents here — the data.json is the canonical
|
|
|
41
41
|
|
|
42
42
|
## Heartbeat (BLOCKING)
|
|
43
43
|
|
|
44
|
-
Write the audit sidecar at `**Audit sidecar path:**` before required reading, then append `- PROGRESS: <stage> <ISO-8601-UTC>` at a cadence no longer than five minutes. Use only: `started`, `required-reading-complete`, `synthesis-start`, `data-json-write-start`, `render-start`, and `write-result-start`. The Phase 7 validator enforces the first stage, timestamps, cadence, and this allowlist.
|
|
44
|
+
Write the audit sidecar at `**Audit sidecar path:**` before required reading, then append `- PROGRESS: <stage> <ISO-8601-UTC>` at a cadence no longer than five minutes. Use only: `started`, `required-reading-complete`, `synthesis-start`, `data-json-write-start`, `render-start`, and `write-result-start`. When a stage runs longer than that, append `- PROGRESS: in-stage:<stage> <ISO-8601-UTC>` with the same stage names. The Phase 7 validator enforces the first stage, timestamps, cadence, and this allowlist.
|
|
45
|
+
|
|
46
|
+
`data-json-write-start`, `render-start` and `write-result-start` open a stretch whose work is one uninterruptible tool call, so no `in-stage:` line can be appended while it runs. Those three carry a 20-minute budget instead of five; every other stage keeps the five-minute cadence.
|
|
45
47
|
|
|
46
48
|
## Execution Rules
|
|
47
49
|
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
#
|
|
18
18
|
# project-root / model-execution-value / prompt-path are required.
|
|
19
19
|
#
|
|
20
|
-
# idle-timeout-seconds is optional (default
|
|
20
|
+
# idle-timeout-seconds is optional (default 600s, or 1500s for the build-running
|
|
21
|
+
# executor/verifier roles — see the role case below). When > 0, an
|
|
21
22
|
# in-process watchdog polls the live-log mtime; if no stdout/stderr write
|
|
22
23
|
# occurs for that many seconds, the underlying `agy` is SIGTERM'd (then
|
|
23
24
|
# SIGKILL'd after a 5-second grace), the status sidecar gets a
|
|
@@ -60,7 +61,16 @@ model="$2"
|
|
|
60
61
|
prompt_path="$3"
|
|
61
62
|
worktree_path="${4-}"
|
|
62
63
|
role="${5:-worker}"
|
|
63
|
-
|
|
64
|
+
# Implementation executor / verifier dispatches run whole build + test suites
|
|
65
|
+
# that legitimately emit no stdout for many minutes; the 600s idle cap reaps
|
|
66
|
+
# them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
|
|
67
|
+
# those roles to a longer idle cap that still sits below the 1800s wall-clock
|
|
68
|
+
# polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
|
|
69
|
+
case "$role" in
|
|
70
|
+
executor | verifier) default_idle_timeout_secs=1500 ;;
|
|
71
|
+
*) default_idle_timeout_secs=600 ;;
|
|
72
|
+
esac
|
|
73
|
+
idle_timeout_secs="${6:-$default_idle_timeout_secs}"
|
|
64
74
|
|
|
65
75
|
if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
|
|
66
76
|
printf 'okstra-antigravity-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
|
|
@@ -12,7 +12,16 @@ model="$2"
|
|
|
12
12
|
prompt_path="$3"
|
|
13
13
|
worktree_path="${4-}"
|
|
14
14
|
role="${5:-worker}"
|
|
15
|
-
|
|
15
|
+
# Implementation executor / verifier dispatches run whole build + test suites
|
|
16
|
+
# that legitimately emit no stdout for many minutes; the 600s idle cap reaps
|
|
17
|
+
# them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
|
|
18
|
+
# those roles to a longer idle cap that still sits below the 1800s wall-clock
|
|
19
|
+
# polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
|
|
20
|
+
case "$role" in
|
|
21
|
+
executor | verifier) default_idle_timeout_secs=1500 ;;
|
|
22
|
+
*) default_idle_timeout_secs=600 ;;
|
|
23
|
+
esac
|
|
24
|
+
idle_timeout_secs="${6:-$default_idle_timeout_secs}"
|
|
16
25
|
|
|
17
26
|
if [[ ! -d "$project_root" ]]; then
|
|
18
27
|
printf 'okstra-claude-exec.sh: project root not found: %s\n' "$project_root" >&2
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
#
|
|
18
18
|
# project-root / model-execution-value / prompt-path are required.
|
|
19
19
|
#
|
|
20
|
-
# idle-timeout-seconds is optional (default
|
|
20
|
+
# idle-timeout-seconds is optional (default 600s, or 1500s for the build-running
|
|
21
|
+
# executor/verifier roles — see the role case below). When > 0, an
|
|
21
22
|
# in-process watchdog polls the live-log mtime; if no stdout/stderr write
|
|
22
23
|
# occurs for that many seconds, the underlying `codex exec` is SIGTERM'd
|
|
23
24
|
# (then SIGKILL'd after a 5-second grace), the status sidecar gets a
|
|
@@ -77,7 +78,16 @@ model="$2"
|
|
|
77
78
|
prompt_path="$3"
|
|
78
79
|
worktree_path="${4-}"
|
|
79
80
|
role="${5:-worker}"
|
|
80
|
-
|
|
81
|
+
# Implementation executor / verifier dispatches run whole build + test suites
|
|
82
|
+
# that legitimately emit no stdout for many minutes; the 600s idle cap reaps
|
|
83
|
+
# them mid-suite (observed: a silent jest + tsc build TERM'd at ~929s). Default
|
|
84
|
+
# those roles to a longer idle cap that still sits below the 1800s wall-clock
|
|
85
|
+
# polling cap so a genuine hang is reaped early. An explicit 6th arg wins.
|
|
86
|
+
case "$role" in
|
|
87
|
+
executor | verifier) default_idle_timeout_secs=1500 ;;
|
|
88
|
+
*) default_idle_timeout_secs=600 ;;
|
|
89
|
+
esac
|
|
90
|
+
idle_timeout_secs="${6:-$default_idle_timeout_secs}"
|
|
81
91
|
|
|
82
92
|
if ! [[ "$idle_timeout_secs" =~ ^[0-9]+$ ]]; then
|
|
83
93
|
printf 'okstra-codex-exec: idle-timeout-seconds must be a non-negative integer: %q\n' "$idle_timeout_secs" >&2
|
|
@@ -84,7 +84,7 @@ Read the worker result files generated in Phase 4/5 and extract individual findi
|
|
|
84
84
|
- Same semantics but disjoint ticket sets → separate groups (do NOT over-merge across tickets).
|
|
85
85
|
- Only one worker confirms a finding → one single-source group.
|
|
86
86
|
4. When grouping is ambiguous, prefer splitting over merging (avoid over-merging). Semantic matching, ticket-set equality, and evidence interpretation remain lead judgments; the engine does not perform fuzzy matching or decide whether evidence is credible.
|
|
87
|
-
5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity.
|
|
87
|
+
5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity. The `audience` enum is a convergence role, not a phase label: every finding-producing worker uses `analysis` — an `implementation` run's verifiers included — and only the report author uses `report-writer`. There is no `implementation-verifier` audience here; map the verifier roster to `analysis`.
|
|
88
88
|
6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed` deterministically marks multi-source groups `full-consensus` and puts only single-source groups in the working queue. Section 6 never enters the grouped input.
|
|
89
89
|
|
|
90
90
|
### Round 1-N: Re-verification Loop (queue-pruned)
|
|
@@ -226,13 +226,13 @@ omits either header, the worker MUST return `<WORKER>_ERRORS_PATH_MISSING`
|
|
|
226
226
|
without proceeding.
|
|
227
227
|
|
|
228
228
|
- `cli-failure` events are recorded by the wrapper subagent itself (Codex / Antigravity), but **directly to the run-level error log** via `okstra error-log append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
|
|
229
|
-
- **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh` take three required positional arguments plus three optional ones, matching their usage line verbatim: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise — when passed it must be an existing directory (the wrapper hard-fails otherwise). For codex it becomes `--add-dir <worktree>` (sandbox write access); for antigravity it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>` and the sibling trace-pane title `<cli>-<role>-tail` (e.g. `codex-executor` ↔ `codex-executor-tail`; antigravity uses the `agy-` prefix). The selected adapter maps the functional assignment identity to this value; when the value is absent, pass the literal `worker` (the wrapper also defaults to `worker` if the argument is omitted). The optional sixth `<idle-timeout-seconds>` overrides the wrapper's stdout-idle watchdog (default
|
|
229
|
+
- **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh` take three required positional arguments plus three optional ones, matching their usage line verbatim: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise — when passed it must be an existing directory (the wrapper hard-fails otherwise). For codex it becomes `--add-dir <worktree>` (sandbox write access); for antigravity it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>` and the sibling trace-pane title `<cli>-<role>-tail` (e.g. `codex-executor` ↔ `codex-executor-tail`; antigravity uses the `agy-` prefix). The selected adapter maps the functional assignment identity to this value; when the value is absent, pass the literal `worker` (the wrapper also defaults to `worker` if the argument is omitted). The optional sixth `<idle-timeout-seconds>` overrides the wrapper's stdout-idle watchdog (default 600s, or 1500s when `<role>` is `executor` / `verifier` — those roles run silent build+test suites — see "No external timeout on wrapper subagents" below).
|
|
230
230
|
- **Background dispatch + polling contract (Codex / Antigravity wrappers).** Both wrapper subagents MUST start their CLI through the selected adapter's asynchronous execution mapping and await the same handle until it reports terminal completion, capped at 30 minutes (1800s) of wall-clock elapsed time. The adapter's await operation is the wait primitive; do not add a standalone sleep or build shorter-sleep loops to bypass a host constraint. This rule applies in **every phase**. Recording responsibilities:
|
|
231
231
|
- Successful completion: return the wrapper's accumulated stdout from the terminal await result. No log entry.
|
|
232
232
|
- Non-zero `exit_code`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
|
|
233
233
|
- Polling cap reached: perform a one-shot **mtime-grace check** on the wrapper's live log (`<prompt>.log`). If the log was written within the last 90 seconds and grace has not yet been applied, extend the cap from 1800s to 2100s and continue awaiting. Otherwise call the selected adapter's termination mapping, record `cli-failure` with `--exit-code 124 --duration-ms <observed_ms> --message "<wrapper> exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, then return the language-specific `*_CLI_TIMEOUT` sentinel.
|
|
234
234
|
- The selected adapter owns runtime-session accounting for the full wrapper window; core retains only the observed start/end event boundaries.
|
|
235
|
-
- **No external timeout on wrapper subagents.** The codex/antigravity wrapper owns BOTH of its timeout mechanisms, and they are the only two: (1) the subagent's polling cap (30min + optional 5min mtime grace), and (2) the wrapper script's internal stdout-idle watchdog — if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI and records a `timeout` stage in the status sidecar (`okstra-codex-exec.sh` / `okstra-antigravity-exec.sh`). A long-running but *chatty* CLI is never idle-killed; a silent hang is reaped at the idle cap instead of burning the full 30 minutes. Lead MUST NOT impose a separate `dispatch_worker` timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before the wrapper's own caps fire. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The caps are calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, pass a lower `<idle-timeout-seconds>` or lower the polling cap in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
|
|
235
|
+
- **No external timeout on wrapper subagents.** The codex/antigravity wrapper owns BOTH of its timeout mechanisms, and they are the only two: (1) the subagent's polling cap (30min + optional 5min mtime grace), and (2) the wrapper script's internal stdout-idle watchdog — if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s, or 1500s for the build-running `executor` / `verifier` roles), the script TERM/KILLs the CLI and records a `timeout` stage in the status sidecar (`okstra-codex-exec.sh` / `okstra-antigravity-exec.sh`). A long-running but *chatty* CLI is never idle-killed; a silent hang is reaped at the idle cap instead of burning the full 30 minutes. Lead MUST NOT impose a separate `dispatch_worker` timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before the wrapper's own caps fire. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The caps are calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, pass a lower `<idle-timeout-seconds>` or lower the polling cap in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
|
|
236
236
|
- `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.
|
|
237
237
|
- Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra error-log append-from-worker` after each worker terminates; Lead does not write into the sidecar.
|
|
238
238
|
|
|
@@ -75,7 +75,6 @@ profile document.
|
|
|
75
75
|
- Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
|
|
76
76
|
- **Worker-side item IDs (free-form but unique within the worker).** Every row item in sections 1–5 (and any optional section 6) of an analysis worker's output MUST carry an item ID that is unique within that one worker's result file. The ID convention is the worker's choice — `F-001` / `F-002` per the suggested schema, `1.1` / `1.2` / `1.3` as Codex tends to use, or any other shape — but it MUST appear as the leading column of the row (for table-form items) or as a `[<ID>]` prefix (for bullet/numbered items). Workers that emit findings without IDs make cross-worker reconciliation impossible.
|
|
77
77
|
- **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises `## 6.1 Consensus` / `## 6.2 Differences` / `## 2.1 Primary Evidence` rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. The `Source items` column (or, where the template still calls it `Supporting workers` / `Workers (position)` / `Source`, that same column) MUST list every contributing worker:item pair (e.g. `claude:F-001, codex:1.1, antigravity:F-3`) so a reviewer can trace the synthesised row back to each worker's original wording without re-reading every worker-results file. Bare worker names without item IDs (e.g. `claude, codex, antigravity`) are rejected. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.SourceItem` pins each entry to `^[a-z][a-z-]*:[A-Za-z0-9._-]+$`, and `ConsensusRow` / `PrimaryEvidenceRow` require a non-empty `sourceItems`, so a bare worker name fails schema validation.
|
|
78
|
-
- **Why this matters.** A real run had `claude=F-1..F-11`, `codex=1.1..1.8`, `antigravity=F-3..F-9` — three incompatible ID schemes. When the lead synthesised `C-1..C-8`, the link from `C-3` back to "which sentence in which worker file" was lost. Source-item preservation restores that link without forcing every worker to adopt a single ID prefix, which would over-constrain worker output style.
|
|
79
78
|
- Audit sidecar (shared): Reading Confirmation placement follows the audience-selected preamble named by `**Worker Preamble Path:**`. Profiles do not restate it; the main worker-results body starts at section 1.
|
|
80
79
|
|
|
81
80
|
- Markdown authoring (shared — applies to markdown documents not already governed by an okstra template/schema):
|
|
@@ -35,6 +35,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
35
35
|
- Doc-only / config-only / pure-rename steps that have no observable runtime behaviour are exempt from the failing-test requirement, but the executor MUST cite the exemption per step in the final report (`TDD exemption: <reason>`).
|
|
36
36
|
- When the touched area has no existing test harness, the executor MUST stand up the minimum harness needed to host one regression test for this run rather than skipping TDD entirely. Record the harness-bootstrap step as an `Out-of-plan edit` if it is not in the plan.
|
|
37
37
|
- **DB / IO / SQL changes require real execution — mock-only is NOT validation evidence:** when this run's diff touches DB/IO/SQL (ORM / query-builder code — sequelize / typeorm / prisma / knex / raw SQL — `*.repository.*`, model/entity files, `migrations/**`, `*.sql`, or any changed query string), a mocked unit test cannot observe the SQL the query builder actually emits (observed failure class: `_implementation-verifier.md` §"DB / IO / SQL change — real-execution gate"). The executor MUST run the change against a real (or faithful-replica) datastore — the `db-test` validation step (plan `validation` db step, else `project.json.qaCommands.db-test`), targeting a **local / replica** DB — and cite its exact command + exit code in the final report's `Validation evidence`. If no real DB / `db-test` command is reachable, do NOT claim the change verified: label the DB portion `static-analysis only …, unverified (not executed)` in the report, surface it in the routing recommendation, and never downplay the real run as "too heavy". `git push` stays forbidden (universal list); the unverified DB state is carried forward so `final-verification` cannot accept it and `release-handoff` cannot push.
|
|
38
|
+
- **External-source adapters — structure AND fixture both derive from a captured real sample; a self-authored fixture is NOT reality evidence:** when this run's diff builds or changes an `external-interface` or `transformation-mapping` surface (an HTTP / network client, or a parser / mapper of a third-party payload — HTML / JSON / XML / CSV originating outside this repo), the adapter's structural assumptions (selectors, field paths, expected response shape) AND the static fixture / golden that tests them MUST BOTH derive from a **captured real sample** of that payload — the capture cited in the stage's `external-interface` / `transformation-mapping` design-prep item, or one captured this run and recorded with its `source` + capture time. The captured sample is a static fixture (no live socket), so a parser test against it stays in source like any unit test — the Real-IO isolation rule below governs *live* calls, not the captured bytes. Do NOT hand-invent the shape and then hand-write a fixture that agrees with it: the passing test then only proves the code matches your assumption, never that the assumption matches reality (self-confirming oracle — the observed failure was a parser whose selectors existed in its synthetic fixture and in zero real pages: hundreds of green units over a fiction, and the whole structure built on the wrong shape). When no real sample is reachable (no network this run, or the brief supplied none), do NOT synthesize a stand-in and present its green tests as correctness: mark the adapter's shape `reality-unverified (no captured sample)` in `Validation evidence`, keep any placeholder fixture explicitly labelled an assumption (never validation evidence), and surface an explicit **user-owned** item in the routing recommendation to confirm against real data. Unlike the DB gate above this does NOT itself block acceptance — live external verification stays a user-owned item per `final-verification`'s External QA advisory policy — but a synthetic external fixture presented as reality-verified is exactly the mock-only external evidence the `final-verification` test-correctness pass is meant to reject.
|
|
38
39
|
- **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa scripts directory `<task_root>/qa/scripts/` (`<TASK_QA_PATH>/scripts`; the `qa/` root itself holds only data sidecars — the Tier 3 conformance manifest and `result-*.json`). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/scripts/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite. **Author qa specs with the project's own test framework — never hand-roll `describe`/`it`/`expect`.** When the project ships a test runner as a devDependency (jest / vitest / pytest …), the qa spec uses it, invoked with the project config plus a discovery override pointing at the qa scripts dir (jest: `npx jest --config <project jest config> --roots <task_root>/qa/scripts --runInBand <spec-name>`) — the project config keeps module aliases resolving while the default sweep never collects the file; never widen the project's own test config to include qa paths. For TypeScript qa specs also write `<task_root>/qa/scripts/tsconfig.json` (`extends` the project tsconfig, adds the runner's `types` entry, `"include": ["**/*.ts"]`) so editors resolve path aliases and test globals — it is a qa artifact like the rest (untracked). **These qa artifacts stay untracked — never commit them.** `.okstra/**` is gitignored (the artifact-home rule); conformance scripts and their results are *executed* and recorded in the carry sidecar / verifier result, never written into git history. A committed `.okstra/qa` file is a stage-branch defect that leaks okstra internals into the eventual PR (see the `git add` rules below).
|
|
39
40
|
- re-read the approved plan end-to-end and parse the `## 5.5 Stage Map`. Read the **Stage** injected in the launch prompt (`Stage for this implementation run`): the single stage number this run owns. The runtime already selected and reserved this stage (one run = one stage) — do NOT recompute the start stage from `consumers.jsonl`.
|
|
40
41
|
- load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(this stage)` and inject them into the executor's working context as "runtime carry-in". For a `depends-on (none)` stage, no sidecar load — task-brief only.
|
|
@@ -55,7 +56,25 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
55
56
|
|
|
56
57
|
## Stage execution contract (this run owns one stage)
|
|
57
58
|
|
|
58
|
-
- **Sidecar evidence writer (BLOCKING).** When this stage's Stage Validation `post` commands all succeed, the Executor MUST emit a JSON object
|
|
59
|
+
- **Sidecar evidence writer (BLOCKING).** When this stage's Stage Validation `post` commands all succeed, the Executor MUST emit a JSON object with **exactly** these fields (spec `docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md` §3.2), as a fenced ```json``` block in the worker result under the heading `### Stage Carry Evidence`, and the lead MUST persist it to `runs/<impl-task-key>/carry/stage-<N>.json`. The schema is inlined here because a CLI executor (codex / antigravity) runs in its own process and never receives the Claude worker-agent definition — a carry emitted with an ad-hoc shape (`stage`/`files`/`validation`) is a `contract-violated` gap the lead must rewrite by hand:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"schemaVersion": 1,
|
|
64
|
+
"sourcePlanPath": "<approved-plan path>",
|
|
65
|
+
"stageNumber": <int>,
|
|
66
|
+
"stageTitle": "<from Stage Map>",
|
|
67
|
+
"completedAt": "<ISO-8601 with tz>",
|
|
68
|
+
"stageCommitRange": { "base": "<sha>", "head": "<sha>" },
|
|
69
|
+
"filesChanged": ["<rel/path>", "..."],
|
|
70
|
+
"newIdentifiers": ["<name>", "..."],
|
|
71
|
+
"stepResults": [{"step": <int>, "status": "done", "commit": "<sha>"}],
|
|
72
|
+
"validationsPassed": ["<label>", "..."],
|
|
73
|
+
"notes": []
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The file MUST NOT exist before the run starts (overwrite is refused — see `--force-stage` non-goal). **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` fails a run that declares `stageSidecarEvidence` without the file on disk. Transcribing the JSON into the report is not the same as writing it: `consumers` treats the carry file as the source of truth for marking the stage `done`, so a missing file leaves the stage permanently incomplete and blocks every dependent stage with a `PrepareError` — while this run reports success.
|
|
59
78
|
- **Reverse link (BLOCKING).** The runtime already appended a `status:"started"` row for this stage before the run began. On completion, append a `status:"done"` row with `carry_path` populated for this stage number.
|
|
60
79
|
- **No PR / push in this phase.** This run produces local commits, carry sidecar evidence, verifier results, and the implementation final report only. Push and PR creation belong exclusively to the later `release-handoff` phase after `final-verification` returns `accepted`.
|
|
61
80
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- requirement & acceptance coverage — every must-pass point in the brief's `## Acceptance Criteria` (and the approved plan's requirements) is covered with a cited artifact or raised as an Acceptance Blocker; no silent omissions
|
|
14
14
|
- delivered artifacts match recorded expected values in `reference-expectations` (config files, deployment manifests, other recorded expected states); when reference-expectations are absent, record it as missing information rather than assuming a match
|
|
15
15
|
- test & validation suite pass status — independently re-run the read-only two-tier command set (Tier 1 = brief/approved-plan `validation`, Tier 2 = `project.json` `qaCommands`) and confirm each passes on the verified head, citing exact command + exit code
|
|
16
|
-
- test correctness — delivered tests actually assert the intended behaviour: no gutted/weakened assertions, no tautological or always-passing tests, no tests exercising only mocks; new behaviour has matching coverage
|
|
16
|
+
- test correctness — delivered tests actually assert the intended behaviour: no gutted/weakened assertions, no tautological or always-passing tests, no tests exercising only mocks; new behaviour has matching coverage. For an `external-interface` / `transformation-mapping` surface specifically, treat a test whose only oracle is a self-authored synthetic fixture (no captured-real-sample provenance) as NOT establishing external correctness — it shows only that the parser agrees with its author's assumed shape, never that the shape matches reality; record that surface's external correctness as a user-owned external advisory gap (a Residual Risk carrying the exact "capture a real sample and confirm" next step, per the Coverage check in the self-review pass below), never as covered — the same non-blocking treatment the External QA advisory policy gives a live external check
|
|
17
17
|
- DB / IO / SQL real-execution evidence — trigger: the diff touches DB/IO/SQL (ORM / query-builder, `*.repository.*`, model / `migrations/**` / `*.sql`, or changed query strings). Then Validation Evidence MUST cite a real (or faithful-replica) DB execution — the `db-test` command + exit code — not a mock-only suite. Rationale: a mock-only suite cannot observe the SQL actually emitted (observed failure class: `prompts/profiles/_implementation-verifier.md` §"DB / IO / SQL change — real-execution gate"). A DB-touching change whose only evidence is mocked, or for which no `db-test` ran, is an **Acceptance Blocker** (`major`+; per the Verdict vocabulary below, any blocker moves the verdict off `accepted`). This gate stops an unverified DB change from reaching `release-handoff` and being pushed.
|
|
18
18
|
- **External Tier 3 de-duplication exception.** A DB/IO/SQL surface covered by an in-scope Tier 3 entry whose `requires` include `db`, `http`, or `external` is governed by the External QA outcome policy. Its non-PASS or unavailable result MUST NOT generate a second legacy db-test-not-configured or mock-only blocker solely for that same Tier 3 non-PASS or unavailable result. Tier 1 or Tier 2 failures remain blocking, and DB surfaces without declared external Tier 3 coverage remain blocking.
|
|
19
19
|
- no new defects introduced — the diff does not break previously-working behaviour and adds no new bug (logic/off-by-one, null/empty handling, resource leaks, broken error paths)
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
- **Disposition:** a row MUST use `inline-contract` only when the stage already states the kind-specific minimum implementation contract; otherwise it MUST use `prep-item` and reference one or more `designPreparation.items`. `not-applicable` is legal only with a concrete rationale consistent with the stage action. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the disposition-specific fields, and `prompts/lead/plan-body-verification.md` `P-Prep-S<stage>-<kind>` verifies semantic sufficiency.
|
|
50
50
|
- **AI-prepared proposal:** every referenced PREP item MUST record `kind`, `stageRefs`, `need`, evidence-cited `knownFacts`, `openQuestions`, a concrete evidence-backed `aiProposal` (`summary`, `details`, `assumptions`, `evidence`, `confidence`), `humanConfirmation`, explicit `status`, and the safest reversible default available. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPrepItem` / `$defs.DesignPrepProposal` enforce required fields, `validators/validate-run.py` `_validate_prep_references` enforces the bidirectional stage/kind link, and `prompts/lead/plan-body-verification.md` rejects empty or non-implementable proposals.
|
|
51
51
|
- **Status choice:** prefer `provisional` with a `workingAssumption`, concrete `guardrails`, `reviewAt`, `ifStillOpen`, and canonical `requestPath`; use `blocked` only for business policy, external authority, a destructive migration decision, or the absence of any safe reversible assumption. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPrepItem` enforces state-specific fields, `validators/validate-run.py` `_validate_design_prep_states` enforces confirmation/request invariants, and `prompts/lead/plan-body-verification.md` judges whether the disposition is justified. A declared `blocked` status does not by itself fail plan-body verification.
|
|
52
|
+
- **External-reality anchoring (`external-interface` / `transformation-mapping` surfaces):** these two detector kinds are correct only against data whose shape lives *outside this repository* (a third-party response body / external payload format). For such a surface the referenced PREP item's `aiProposal` MUST derive the assumed shape — selectors, field paths, response structure — from a **captured real sample** and cite it in `knownFacts` / `evidence` (source + capture time); a shape invented from internal reasoning and marked `confidence: high` is the disallowed move, because a plan built on an assumed shape yields an implementation whose parser and fixture only ever agree with each other. When the brief supplies no sample and none is capturable at plan time, the item MUST stay `provisional` with a `workingAssumption` that the external shape is unverified against reality, a `guardrails` line forbidding the implementation from presenting a synthetic-fixture green run as reality-verified, and an `ifStillOpen` that routes to user confirmation against real data — it MUST NOT be dispositioned `inline-contract` / settled. **Enforced (semantic):** the surface's presence is machine-checked by `_validate_detector_coverage`; whether its proposal's evidence is genuinely external is judged by the §5.5.9 `P-Prep-S<stage>-<kind>` round (this phase runs it adversarially).
|
|
52
53
|
- **Planner-only test surface:** add `manual-user-test` only when a test prerequisite changes the implementation interface or acceptance contract; the V1 detector never emits it. **Enforced:** `validators/validate-run.py` `_validate_detector_coverage` rejects detector-produced `manual-user-test`, `schemas/final-report-v1.0.schema.json` permits its planner-authored shape, and `prompts/lead/plan-body-verification.md` verifies the stage-action rationale.
|
|
53
54
|
- **Trivial task:** when the detector returns no surfaces and no interface/acceptance-changing manual test input exists, `designPreparation` MUST use `mode: no-design-inputs`, an empty `items` array, and a concrete reason tied to the plan. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.DesignPreparation` requires the reason and empty array for that mode; `validators/validate-run.py` `_validate_design_prep_contract` validates the marked V1 payload.
|
|
54
55
|
- Approval gate (phase-specific addendum to shared authority rule):
|
|
@@ -258,23 +258,44 @@ def parse_clarification_rows(report_text: str) -> list[dict]:
|
|
|
258
258
|
|
|
259
259
|
UNRESOLVED_STATUSES = {"open", "answered"}
|
|
260
260
|
|
|
261
|
+
# `Blocks` values that gate the user's `approved: true` flip.
|
|
262
|
+
APPROVAL_BLOCKS = frozenset({"approval"})
|
|
263
|
+
# `Blocks` values that owe the user an answer at all. `next-phase` rows never
|
|
264
|
+
# gate approval, but the next phase cannot start with them open either, so the
|
|
265
|
+
# user-response skill must list them — counting only `approval` made a report
|
|
266
|
+
# whose open items were all `next-phase` look like it had none.
|
|
267
|
+
USER_INPUT_BLOCKS = frozenset({"approval", "next-phase"})
|
|
268
|
+
|
|
261
269
|
|
|
262
270
|
@dataclass(frozen=True)
|
|
263
|
-
class
|
|
264
|
-
"""Fail-closed read of
|
|
271
|
+
class ClarificationScan:
|
|
272
|
+
"""Fail-closed read of §1 for rows blocking on one `Blocks` value set.
|
|
265
273
|
|
|
266
274
|
``unreadable_reason`` is ``None`` only when the scan is confident: §1
|
|
267
275
|
parsed cleanly (or is the legitimate table-less placeholder) and
|
|
268
276
|
``blockers`` is therefore authoritative. A non-None reason means the
|
|
269
|
-
|
|
277
|
+
caller must refuse to act — never soft-pass.
|
|
270
278
|
"""
|
|
271
279
|
blockers: list[ClarificationItem]
|
|
272
280
|
unreadable_reason: Optional[str]
|
|
273
281
|
|
|
274
282
|
|
|
275
|
-
def scan_approval_gate(report_text: str) ->
|
|
283
|
+
def scan_approval_gate(report_text: str) -> ClarificationScan:
|
|
276
284
|
"""Scan §1 for unresolved ``Blocks=approval`` rows (``Status`` in
|
|
277
285
|
``{open, answered}``), refusing to guess whenever the schema drifted."""
|
|
286
|
+
return scan_clarification_blockers(report_text, APPROVAL_BLOCKS)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def scan_open_user_input(report_text: str) -> ClarificationScan:
|
|
290
|
+
"""Scan §1 for every unresolved row that still owes the user an answer
|
|
291
|
+
(``Blocks`` in ``{approval, next-phase}``)."""
|
|
292
|
+
return scan_clarification_blockers(report_text, USER_INPUT_BLOCKS)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def scan_clarification_blockers(
|
|
296
|
+
report_text: str, blocking_values: frozenset[str]
|
|
297
|
+
) -> ClarificationScan:
|
|
298
|
+
"""Shared fail-closed §1 walk for both gates above."""
|
|
278
299
|
section = _section_1_slice(report_text)
|
|
279
300
|
if section is None:
|
|
280
301
|
if _LOOSE_SECTION_1_RE.search(report_text):
|
|
@@ -283,32 +304,33 @@ def scan_approval_gate(report_text: str) -> ApprovalGateScan:
|
|
|
283
304
|
"the schema heading format (anchor/format drift)"
|
|
284
305
|
)
|
|
285
306
|
else:
|
|
307
|
+
expected = "/".join(sorted(blocking_values))
|
|
286
308
|
reason = (
|
|
287
309
|
"report has no `## 1. Clarification Items` section — the gate "
|
|
288
|
-
"cannot confirm there are no unresolved `Blocks=
|
|
310
|
+
f"cannot confirm there are no unresolved `Blocks={expected}` rows"
|
|
289
311
|
)
|
|
290
|
-
return
|
|
312
|
+
return ClarificationScan([], reason)
|
|
291
313
|
table = _walk_section_1_table(section)
|
|
292
314
|
if table.items is None:
|
|
293
315
|
if table.has_pipe_lines:
|
|
294
|
-
return
|
|
316
|
+
return ClarificationScan([], (
|
|
295
317
|
"§1 contains a table but its header row is not the schema "
|
|
296
318
|
"header (`| ... | Statement | Expected form | User input |`)"
|
|
297
319
|
))
|
|
298
320
|
# Renderer's emptyState placeholder: heading is intact and no table
|
|
299
|
-
# was emitted — confidently "no
|
|
300
|
-
return
|
|
321
|
+
# was emitted — confidently "no blocking items".
|
|
322
|
+
return ClarificationScan([], None)
|
|
301
323
|
if table.unparsed_row_count:
|
|
302
|
-
return
|
|
324
|
+
return ClarificationScan([], (
|
|
303
325
|
f"§1 table has {table.unparsed_row_count} row(s) whose metadata "
|
|
304
326
|
"cell could not be parsed (Blocks/Status markers missing or "
|
|
305
327
|
"malformed)"
|
|
306
328
|
))
|
|
307
329
|
blockers = [
|
|
308
330
|
it for it in table.items
|
|
309
|
-
if it.blocks
|
|
331
|
+
if it.blocks in blocking_values and it.status in UNRESOLVED_STATUSES
|
|
310
332
|
]
|
|
311
|
-
return
|
|
333
|
+
return ClarificationScan(blockers, None)
|
|
312
334
|
|
|
313
335
|
|
|
314
336
|
# 느슨한 §1 헤딩 탐지: 엄격한 SECTION_HEADING_PATTERN 이 실패해도 이게 매칭되면
|
|
@@ -457,7 +457,12 @@ def validate_working_state(state: Mapping[str, Any]) -> list[str]:
|
|
|
457
457
|
else:
|
|
458
458
|
seen_workers.add(worker_id)
|
|
459
459
|
if not _nonempty_string(audience) or audience not in _AUDIENCES:
|
|
460
|
-
errors.append(
|
|
460
|
+
errors.append(
|
|
461
|
+
f"workers[{index}].audience is unsupported: {audience!r}. "
|
|
462
|
+
"Finding-producing workers (implementation verifiers included) "
|
|
463
|
+
"use 'analysis'; only the report author uses 'report-writer'. "
|
|
464
|
+
f"Allowed: {sorted(_AUDIENCES)}."
|
|
465
|
+
)
|
|
461
466
|
findings = state.get("findings")
|
|
462
467
|
if not isinstance(findings, list):
|
|
463
468
|
errors.append("findings must be an array")
|
|
@@ -1821,7 +1826,11 @@ def _parse_workers(value: Any) -> list[dict[str, str]]:
|
|
|
1821
1826
|
audience = _required_string(worker, "audience", f"workers[{index}]")
|
|
1822
1827
|
if audience not in _AUDIENCES:
|
|
1823
1828
|
raise ConvergenceContractError(
|
|
1824
|
-
f"workers[{index}] has unsupported audience: {audience}"
|
|
1829
|
+
f"workers[{index}] has unsupported audience: {audience}. "
|
|
1830
|
+
"Convergence audience is a functional role, not a phase label: "
|
|
1831
|
+
"every finding-producing worker uses 'analysis' (an "
|
|
1832
|
+
"implementation run's verifiers included), and only the report "
|
|
1833
|
+
f"author uses 'report-writer'. Allowed: {sorted(_AUDIENCES)}."
|
|
1825
1834
|
)
|
|
1826
1835
|
if worker_id in seen:
|
|
1827
1836
|
raise ConvergenceContractError(f"duplicate workerId: {worker_id}")
|
|
@@ -22,6 +22,8 @@ CLAUDE = {
|
|
|
22
22
|
"fable-5": ModelSpec("fable-5", "claude-fable-5"),
|
|
23
23
|
"claude-fable-5": ModelSpec("fable-5", "claude-fable-5"),
|
|
24
24
|
"opus": ModelSpec("opus", "opus", in_picker=True),
|
|
25
|
+
"opus-5": ModelSpec("opus-5", "claude-opus-5"),
|
|
26
|
+
"claude-opus-5": ModelSpec("opus-5", "claude-opus-5"),
|
|
25
27
|
"opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
|
|
26
28
|
"claude-opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
|
|
27
29
|
"opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
|
|
@@ -492,7 +492,7 @@ def _enforce_schema(data: dict) -> None:
|
|
|
492
492
|
# 있으나 표시이므로 실행에는 무해하다. 새 버전이 나오면 여기만 갱신한다.
|
|
493
493
|
_DISPLAY_CONCRETE_CLAUDE = {
|
|
494
494
|
"fable": "claude-fable-5",
|
|
495
|
-
"opus": "claude-opus-
|
|
495
|
+
"opus": "claude-opus-5",
|
|
496
496
|
"sonnet": "claude-sonnet-4-6",
|
|
497
497
|
"haiku": "claude-haiku-4-5",
|
|
498
498
|
}
|
|
@@ -891,6 +891,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
891
891
|
|
|
892
892
|
|
|
893
893
|
_INCLUDE_DIRECTIVE = re.compile(r"\{\{INCLUDE:([^}]+?)\}\}")
|
|
894
|
+
_HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
894
895
|
|
|
895
896
|
|
|
896
897
|
def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
|
|
@@ -901,6 +902,12 @@ def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
|
|
|
901
902
|
contents replace the directive line in-place. Missing include targets
|
|
902
903
|
raise PrepareError so a bad reference fails fast instead of silently
|
|
903
904
|
leaving a `{{INCLUDE:...}}` token in the rendered profile.
|
|
905
|
+
|
|
906
|
+
The top-level result is stripped of HTML comments: the shared fragments
|
|
907
|
+
carry maintainer-only `<!-- ... -->` notes (edit-here-once guidance, dedup
|
|
908
|
+
rationale) that would otherwise ride into the rendered profile the lead
|
|
909
|
+
reads every run without changing a single instruction. Nested-include
|
|
910
|
+
bodies are left intact and get stripped once by this depth-0 pass.
|
|
904
911
|
"""
|
|
905
912
|
if _depth > 4:
|
|
906
913
|
raise PrepareError(
|
|
@@ -917,7 +924,11 @@ def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
|
|
|
917
924
|
)
|
|
918
925
|
return _expand_profile_includes(target, _depth + 1).rstrip("\n")
|
|
919
926
|
|
|
920
|
-
|
|
927
|
+
expanded = _INCLUDE_DIRECTIVE.sub(_sub, text)
|
|
928
|
+
if _depth == 0:
|
|
929
|
+
expanded = _HTML_COMMENT.sub("", expanded)
|
|
930
|
+
expanded = re.sub(r"\n{3,}", "\n\n", expanded)
|
|
931
|
+
return expanded
|
|
921
932
|
|
|
922
933
|
|
|
923
934
|
# ---------------------------------------------------------------------------
|
|
@@ -1326,7 +1337,12 @@ def _resolve_pr_template(inp: PrepareInputs) -> tuple[str, str]:
|
|
|
1326
1337
|
|
|
1327
1338
|
@dataclass
|
|
1328
1339
|
class _ModelBindings:
|
|
1329
|
-
"""이 run 의 lead/worker 모델 + critic + executor 바인딩.
|
|
1340
|
+
"""이 run 의 lead/worker 모델 + critic + executor 바인딩.
|
|
1341
|
+
|
|
1342
|
+
`*_execution` 은 dispatch 에 쓰이는 CLI 신원 철자다. 카탈로그 값과 달리
|
|
1343
|
+
provider CLI 조회를 거치므로 실패할 수 있고, 그래서 이 묶음에서 함께
|
|
1344
|
+
해소한다 — prepare 가 run seq 를 할당하거나 worktree 를 만들기 전에.
|
|
1345
|
+
"""
|
|
1330
1346
|
|
|
1331
1347
|
lead: ModelAssignment
|
|
1332
1348
|
cw: ModelAssignment
|
|
@@ -1338,6 +1354,9 @@ class _ModelBindings:
|
|
|
1338
1354
|
executor_display_name: str
|
|
1339
1355
|
executor_worker_agent: str
|
|
1340
1356
|
executor_model_meta: ModelAssignment
|
|
1357
|
+
codex_worker_execution: str
|
|
1358
|
+
antigravity_worker_execution: str
|
|
1359
|
+
executor_execution: str
|
|
1341
1360
|
|
|
1342
1361
|
|
|
1343
1362
|
def recommended_role_models() -> dict[str, str]:
|
|
@@ -1413,6 +1432,12 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
|
|
|
1413
1432
|
executor_display_name=display_name,
|
|
1414
1433
|
executor_worker_agent=worker_agent,
|
|
1415
1434
|
executor_model_meta=model_meta,
|
|
1435
|
+
codex_worker_execution=_worker_execution_value(
|
|
1436
|
+
m["co"], "codex", "worker", workers),
|
|
1437
|
+
antigravity_worker_execution=_worker_execution_value(
|
|
1438
|
+
m["ge"], "antigravity", "worker", workers),
|
|
1439
|
+
executor_execution=_normalized_execution(
|
|
1440
|
+
model_meta, provider=executor_provider, role="executor"),
|
|
1416
1441
|
)
|
|
1417
1442
|
|
|
1418
1443
|
|
|
@@ -2176,22 +2201,16 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2176
2201
|
"CLAUDE_WORKER_MODEL": cw.display,
|
|
2177
2202
|
"CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
|
|
2178
2203
|
"CODEX_WORKER_MODEL": co.display,
|
|
2179
|
-
"CODEX_WORKER_MODEL_EXECUTION_VALUE":
|
|
2180
|
-
co, "codex", "worker", workers,
|
|
2181
|
-
),
|
|
2204
|
+
"CODEX_WORKER_MODEL_EXECUTION_VALUE": models.codex_worker_execution,
|
|
2182
2205
|
"ANTIGRAVITY_WORKER_MODEL": ge.display,
|
|
2183
|
-
"ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE":
|
|
2184
|
-
ge, "antigravity", "worker", workers,
|
|
2185
|
-
),
|
|
2206
|
+
"ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": models.antigravity_worker_execution,
|
|
2186
2207
|
"REPORT_WRITER_MODEL": rw.display,
|
|
2187
2208
|
"REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
|
|
2188
2209
|
"EXECUTOR_PROVIDER": executor_provider,
|
|
2189
2210
|
"EXECUTOR_DISPLAY_NAME": executor_display_name,
|
|
2190
2211
|
"EXECUTOR_WORKER_AGENT": executor_worker_agent,
|
|
2191
2212
|
"EXECUTOR_MODEL_DISPLAY": executor_model_meta.display,
|
|
2192
|
-
"EXECUTOR_MODEL_EXECUTION_VALUE":
|
|
2193
|
-
executor_model_meta, provider=executor_provider, role="executor",
|
|
2194
|
-
),
|
|
2213
|
+
"EXECUTOR_MODEL_EXECUTION_VALUE": models.executor_execution,
|
|
2195
2214
|
"CRITIC_CHOICE": critic_choice,
|
|
2196
2215
|
"RELATED_TASKS_JSON": related_tasks_json_str,
|
|
2197
2216
|
"RELATED_TASKS_BULLETS": bullets,
|