okstra 0.134.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 +8 -3
- 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/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/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 +12 -1
- package/runtime/python/okstra_ctl/worker_artifact_paths.py +5 -1
- package/runtime/python/okstra_token_usage/collect.py +25 -15
- 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 +65 -17
- package/runtime/validators/forbidden_actions.py +1 -1
- package/runtime/validators/validate-run.py +11 -2
- package/runtime/validators/validate_session_conformance.py +1 -1
|
@@ -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
|
|
|
@@ -35,8 +37,11 @@ okstra preflight --runtime claude-code --json
|
|
|
35
37
|
## Flow
|
|
36
38
|
|
|
37
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).
|
|
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. **
|
|
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.
|
|
@@ -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):
|
|
@@ -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
|
# ---------------------------------------------------------------------------
|
|
@@ -17,7 +17,11 @@ def audit_sidecar_rel(worker_result_rel: str) -> str:
|
|
|
17
17
|
prefix, separator, suffix = basename.partition("-worker-")
|
|
18
18
|
if not separator:
|
|
19
19
|
raise WorkerArtifactPathError(
|
|
20
|
-
|
|
20
|
+
"worker result path has no canonical -worker- token: "
|
|
21
|
+
f"{worker_result_rel}. The audit sidecar is derived from the "
|
|
22
|
+
"canonical worker-result name `<role>-worker-<task-type>-<seq>.md` "
|
|
23
|
+
"(e.g. `codex-worker-implementation-001.md`); name the result that "
|
|
24
|
+
"way — a role label like `codex-verifier-...` drops the -worker- token."
|
|
21
25
|
)
|
|
22
26
|
audit_filename = f"{prefix}-worker-audit-{suffix}"
|
|
23
27
|
return f"{directory}{slash}{audit_filename}" if slash else audit_filename
|
|
@@ -227,7 +227,9 @@ def _previous_run_end(run_dir: Path, suffix: str) -> str | None:
|
|
|
227
227
|
return _run_end_estimate(run_dir, prev) or _run_manifest_created_at(run_dir, prev)
|
|
228
228
|
|
|
229
229
|
|
|
230
|
-
def resolve_run_window(
|
|
230
|
+
def resolve_run_window(
|
|
231
|
+
team_state_path: Path, state: dict, *, relax_start: bool = True
|
|
232
|
+
) -> tuple[str | None, str | None]:
|
|
231
233
|
"""이 run 의 [시작, 종료] ISO 윈도우.
|
|
232
234
|
|
|
233
235
|
in-session lead 는 자기 run 을 사용자의 *세션 전체* jsonl 에 기록하므로,
|
|
@@ -238,26 +240,34 @@ def resolve_run_window(team_state_path: Path, state: dict) -> tuple[str | None,
|
|
|
238
240
|
status mtime → 현재 시각(아직 진행 중) 순으로 해소한다. 접미사를 못 뽑으면
|
|
239
241
|
(None, None) — 윈도우 없이 전체를 세는 기존 동작으로 안전 폴백.
|
|
240
242
|
|
|
241
|
-
시작
|
|
242
|
-
늦게 찍히면(관측: dev-9902 — createdAt
|
|
243
|
-
since 가 앞선 세션을 잘라낸다. Task 3 이
|
|
244
|
-
첫 ts 최소값이 createdAt 보다 앞서면 그 값으로
|
|
245
|
-
`leadSessionIds` 부재(legacy)면 createdAt 그대로 — 기존
|
|
246
|
-
|
|
247
|
-
완화 하한: 직전 run 의 종료 시각 이전으로는 내려가지 않는다
|
|
243
|
+
시작 완화(`relax_start`, 기본 True — 토큰 수집용): run-manifest createdAt 이
|
|
244
|
+
이 run 의 실제 lead/worker 세션보다 늦게 찍히면(관측: dev-9902 — createdAt
|
|
245
|
+
11:43Z 가 05:34Z 시작 세션보다 늦음) since 가 앞선 세션을 잘라낸다. Task 3 이
|
|
246
|
+
기록한 `leadSessionIds[]` 세션의 첫 ts 최소값이 createdAt 보다 앞서면 그 값으로
|
|
247
|
+
since 를 앞당긴다. `leadSessionIds` 부재(legacy)면 createdAt 그대로 — 기존
|
|
248
|
+
동작 불변. 완화 하한: 직전 run 의 종료 시각 이전으로는 내려가지 않는다
|
|
248
249
|
(`_previous_run_end`). 세션 jsonl 은 run 마다 새로 생기지 않으므로 하한이
|
|
249
|
-
없으면 같은 세션의 이전 run 이 이 윈도우에 통째로 들어온다.
|
|
250
|
+
없으면 같은 세션의 이전 run 이 이 윈도우에 통째로 들어온다.
|
|
251
|
+
|
|
252
|
+
`relax_start=False` (세션 스코프 검증기 전용 — session-conformance /
|
|
253
|
+
forbidden-actions): 완화를 건너뛰고 since 를 createdAt 에 고정한다. createdAt
|
|
254
|
+
은 prep 시각이라 이 run 의 lead·worker 활동보다 반드시 앞서므로 건전한 하한이고,
|
|
255
|
+
재사용 세션에 섞인 같은 세션의 이전 run·다른 task 활동을 창에서 배제한다
|
|
256
|
+
(dev-10172 D-3: 이전 planning run 의 phase-6/7 앵커, 다른 task 의 git push 를
|
|
257
|
+
이 run 의 위반으로 오탐하던 근본 원인). 완화는 토큰을 놓치지 않으려고 세션
|
|
258
|
+
birth 까지 내려가지만, 검증기에는 그 관대함이 곧 오탐이다."""
|
|
250
259
|
suffix = run_artifact_suffix(team_state_path)
|
|
251
260
|
if not suffix:
|
|
252
261
|
return None, None
|
|
253
262
|
run_dir = team_state_path.parent.parent
|
|
254
263
|
since = _run_manifest_created_at(run_dir, suffix)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
since
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
since
|
|
264
|
+
if relax_start:
|
|
265
|
+
earliest = _earliest_lead_session_ts(state, team_state_path)
|
|
266
|
+
if earliest and (since is None or earliest < since):
|
|
267
|
+
since = earliest
|
|
268
|
+
floor = _previous_run_end(run_dir, suffix)
|
|
269
|
+
if floor and since and since < floor:
|
|
270
|
+
since = floor
|
|
261
271
|
until = state.get("runEndedAt") or _run_end_estimate(run_dir, suffix) or utc_now()
|
|
262
272
|
return since, until
|
|
263
273
|
|
|
@@ -4,7 +4,7 @@ Pricing is matched by substring against the model id recorded in the session
|
|
|
4
4
|
transcript, so keys must reflect the *actual* model id form emitted by each
|
|
5
5
|
provider:
|
|
6
6
|
|
|
7
|
-
* Anthropic — `claude-fable-5*`, `claude-opus-4-*`, `claude-sonnet-4-*`,
|
|
7
|
+
* Anthropic — `claude-fable-5*`, `claude-opus-5*`, `claude-opus-4-*`, `claude-sonnet-4-*`,
|
|
8
8
|
`claude-haiku-4-5-*`, `claude-3-5-sonnet-*`, `claude-3-5-haiku-*`,
|
|
9
9
|
`claude-3-opus-*`, `claude-3-haiku-*`.
|
|
10
10
|
* OpenAI / Codex — `gpt-5*`, `gpt-4o*`, `gpt-4*`.
|
|
@@ -52,6 +52,9 @@ CLAUDE_PRICING = {
|
|
|
52
52
|
# Claude Fable 5 (tier above Opus).
|
|
53
53
|
"fable-5": (10.0, 12.5, 1.0, 50.0), # Fable 5 (cache prices derived from ratios)
|
|
54
54
|
|
|
55
|
+
# Claude Opus 5.
|
|
56
|
+
"opus-5": (5.0, 6.25, 0.50, 25.0), # Opus 5 (cache prices derived from ratios)
|
|
57
|
+
|
|
55
58
|
# Claude 4 point releases (explicit so future divergence is easy to see).
|
|
56
59
|
"opus-4-8": (5.0, 6.25, 0.50, 25.0), # Opus 4.8 (cache prices derived from ratios)
|
|
57
60
|
"opus-4-7": (5.0, 6.25, 0.50, 25.0), # Opus 4.7 (cache prices derived from ratios)
|
|
@@ -96,6 +96,30 @@ diff and commits: describe only what actually changed. Mark checklist boxes
|
|
|
96
96
|
docs box). If `commits`/`diffStat` are empty, tell the user there is nothing to
|
|
97
97
|
describe and stop. **Never** append AI trailers/footers.
|
|
98
98
|
|
|
99
|
+
### A3b. Identifier allowlist for the body
|
|
100
|
+
|
|
101
|
+
Reviewers have never seen okstra, so an okstra-internal id reads as a nonsense
|
|
102
|
+
token. Identifiers in the PR title and body come from this list and nothing else:
|
|
103
|
+
|
|
104
|
+
| allowed | example |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| repo-relative source path, optionally with a line | `src/commands/pr/pr.mjs:42` |
|
|
107
|
+
| symbol name that exists in the diff | `resolveTemplate()` |
|
|
108
|
+
| branch name, commit subject, commit SHA | `feature/dev-9436`, `a1b2c3d` |
|
|
109
|
+
| issue-tracker ticket id the reviewer can open | `dev-9436` |
|
|
110
|
+
|
|
111
|
+
okstra's own artifact identifiers are out of the allowlist — drop them:
|
|
112
|
+
|
|
113
|
+
- report item ids — `F-001`, `C-001`, `R-001`, `D-0001`, `PREP-001`
|
|
114
|
+
- run artifact names and their `<task-type>-<seq>` suffixes —
|
|
115
|
+
`final-report-implementation-3.md`, `run-manifest-implementation-3.json`
|
|
116
|
+
- phase / stage / worker labels — `final-verification`, `stage-2`, `codex-worker`
|
|
117
|
+
- any path under `.okstra/` — reports, decisions, glossary, user-response sidecars
|
|
118
|
+
|
|
119
|
+
When the only source for a claim is such an artifact, restate its substance in
|
|
120
|
+
the reviewer's terms — what changed in the code, and why — instead of citing the
|
|
121
|
+
id.
|
|
122
|
+
|
|
99
123
|
### A4. Output and offer to create the PR
|
|
100
124
|
|
|
101
125
|
Print the filled PR body to chat as a single fenced markdown block. Then ask the
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: okstra-user-response
|
|
3
3
|
description: >-
|
|
4
|
-
Use this to answer an okstra task's open clarification questions in-session, without hand-editing any file. The tell is a request to respond to an okstra run's clarification items or its approval gate — "answer okstra", "I'll answer the questions", "clarification response", "user response", "approve and move on". This skill lists tasks whose latest report still has open clarification blockers,
|
|
4
|
+
Use this to answer an okstra task's open clarification questions in-session, without hand-editing any file. The tell is a request to respond to an okstra run's clarification items or its approval gate — "answer okstra", "I'll answer the questions", "clarification response", "user response", "approve and move on". This skill lists tasks whose latest report still has open clarification blockers, then walks the open C-ids one at a time — each rewritten as a self-contained question with background, three concrete answers (recommendation first) plus "Enter directly" — echoes the collected answers back for an explicit "confirmed" acknowledgement, optionally records approval, and writes the `user-responses/` sidecar via `okstra user-response write`. NOT for starting a run (okstra-run), inspecting a finished task (okstra-inspect), or generating a brief (okstra-brief-gen). The skill never picks an answer — it builds the option board and transcribes what the user decides.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# OKSTRA User Response
|
|
8
8
|
|
|
9
9
|
Single entry point for answering the clarification questions an okstra run left behind (the open `C-*` rows under the final report's `## 1. Clarification Items`) **in-session**, and recording those answers as a `runs/<type>/user-responses/` sidecar. The next `/okstra-run` auto-attaches this sidecar via `--clarification-response`.
|
|
10
10
|
|
|
11
|
-
**Core principle — the skill never picks an answer for the user.**
|
|
11
|
+
**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 never calls `write` until the user has explicitly confirmed.
|
|
12
|
+
|
|
13
|
+
**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. Ask item 1, transcribe the answer, then ask item 2.
|
|
12
14
|
|
|
13
15
|
| Sub-command | What it does |
|
|
14
16
|
|---|---|
|
|
@@ -63,7 +65,7 @@ Returns a JSON array (latest report mtime first); each entry:
|
|
|
63
65
|
|
|
64
66
|
Carry the chosen entry's `reportPath` and `taskKey` forward.
|
|
65
67
|
|
|
66
|
-
## Step 2:
|
|
68
|
+
## Step 2: Fetch the open rows (data only — ask nothing yet)
|
|
67
69
|
|
|
68
70
|
```bash
|
|
69
71
|
okstra user-response show --report <reportPath>
|
|
@@ -71,33 +73,79 @@ okstra user-response show --report <reportPath>
|
|
|
71
73
|
|
|
72
74
|
Returns `{reportPath, rows: [{id, kind, blocks, status, statement, recommended, alternatives, contextRefs, resolvedRefs}]}`. `resolvedRefs` is `[{ref, definition}]` — the CLI has already looked up what each internal token (`RB-002`, `FU-001`, `§4.7`, …) means in the report body; `definition` is `null` only when the report text alone could not resolve it (e.g. a `path:line` pointer).
|
|
73
75
|
|
|
74
|
-
**Do
|
|
76
|
+
This call is a **data fetch, not a presentation step**. Do not print `rows` at the user, and do not paste a raw `statement` as a question — a `statement` like `Rewrite RB-002 rollback (see §4.7)` is meaningless on its own, which is the whole reason this skill exists. Announce only the count and the plan:
|
|
77
|
+
|
|
78
|
+
> `<N>` open items — I'll go through them one at a time.
|
|
79
|
+
|
|
80
|
+
Step 3 then turns exactly one row at a time into a question.
|
|
81
|
+
|
|
82
|
+
## Step 3: Ask one item at a time (loop)
|
|
83
|
+
|
|
84
|
+
Walk the open rows in report order. **One item per `AskUserQuestion` call** — never batch two items into one call, and never pre-announce the items still to come. Ask, transcribe, move on. Head each item with its position and what it is holding up:
|
|
85
|
+
|
|
86
|
+
> **[2/5] C-014** — blocks: approval gate
|
|
87
|
+
|
|
88
|
+
### 3a. Background first (plain text, above the picker)
|
|
89
|
+
|
|
90
|
+
`AskUserQuestion` carries one sentence of question, so the background goes in the message text right before the call. Write these three blocks, **3–6 lines total** — enough that someone who never read the report can answer:
|
|
91
|
+
|
|
92
|
+
1. **Situation** — what the run was doing when it stopped at this item, in the user's own domain terms.
|
|
93
|
+
2. **What is undecided** — the actual fork, with every internal token expanded inline from `resolvedRefs[].definition`; the user must never need to know what `RB-002` is to answer. Say what is stuck right now in plain words: `blocks: approval` → the approval gate stays shut and `implementation` cannot start; `blocks: next-phase` → the next phase cannot begin.
|
|
94
|
+
3. **What changes with your answer** — what each direction actually causes downstream.
|
|
95
|
+
|
|
96
|
+
Where the background comes from — in this order:
|
|
97
|
+
|
|
98
|
+
- `resolvedRefs[].definition`, which is already resolved and needs no file read;
|
|
99
|
+
- when a `definition` is `null`, or the three blocks would otherwise be empty, **Read** the report `§`/`path:line` that this item's `contextRefs[]` points at and take it from there;
|
|
100
|
+
- **never invent it.** If the report genuinely does not say why, write `The report states nothing beyond the statement itself.` and go straight to the question. Fabricated background corrupts the answer it collects.
|
|
101
|
+
|
|
102
|
+
Close the background with the raw source on one line, so the mapping back to the report stays visible:
|
|
103
|
+
|
|
104
|
+
> Source: `C-014` — "<raw statement>"
|
|
105
|
+
|
|
106
|
+
### 3b. The picker — three answers plus `Enter directly`
|
|
107
|
+
|
|
108
|
+
One `AskUserQuestion` (single-select), with exactly four options in this order:
|
|
109
|
+
|
|
110
|
+
1. **The recommendation** — the answer from `recommended` restated in plain language, label suffixed `(Recommended)`; its description carries the rationale (the part of the `recommended` cell after `—`).
|
|
111
|
+
2. **Alternative** — `alternatives[0]`.
|
|
112
|
+
3. **Alternative** — `alternatives[1]`.
|
|
113
|
+
4. **`Enter directly`** — always last, always present.
|
|
114
|
+
|
|
115
|
+
Slots 2–3 fill from `alternatives[]` first. When `alternatives[]` is short, fill each remaining slot with the first of these that applies:
|
|
116
|
+
|
|
117
|
+
- **a concrete candidate the report itself names** — another option row, an approach §2 rejected, a value already in use elsewhere. Its description must open with `Not an okstra proposal — from <where>` and cite that source.
|
|
118
|
+
- **`Reframe this question`** — the item is held rather than answered, and the next run re-asks it.
|
|
119
|
+
|
|
120
|
+
That last entry is what guarantees the picker always reaches three answers plus `Enter directly`. Never pad a slot with an option no source supports, and never mark anything but `recommended` as recommended.
|
|
121
|
+
|
|
122
|
+
Keep each `label` to the answer itself (1–5 words); the description carries the consequence.
|
|
123
|
+
|
|
124
|
+
### 3c. Transcribe the decision, then move on
|
|
75
125
|
|
|
76
|
-
|
|
77
|
-
- **`recommended`** — the answer okstra proposed (+ rationale), also stated in plain language. It is only a recommendation.
|
|
78
|
-
- **`alternatives[]`** — list them if present, each in plain language.
|
|
79
|
-
- **Raw source** (secondary, for traceability): the raw `id` + `statement` so the mapping back to the report stays visible.
|
|
126
|
+
Record one entry — `{id, kind, value, rationale?, disposition}`, `kind` copied from the row `show` returned:
|
|
80
127
|
|
|
81
|
-
|
|
128
|
+
| The user picks | `value` | `disposition` |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| Option 1–3 | that option's full answer text, not its short label | `answer` |
|
|
131
|
+
| `Enter directly` → their own answer | the user's utterance verbatim (rationale into `rationale`) | `answer` |
|
|
132
|
+
| `Reframe this question`, or free text asking for it to be re-asked | what the user wants re-asked, verbatim (empty → the raw statement) | `reframe` |
|
|
82
133
|
|
|
83
|
-
|
|
134
|
+
A reframe is not an answer, so it does not satisfy the approval gate.
|
|
84
135
|
|
|
85
|
-
|
|
86
|
-
2. **Ask-to-explain (fallback)** — Step 2 already expanded every internal token, so the question should already be self-contained. If the user still asks "what does this mean?" or a `resolvedRefs` `definition` was `null`, open the report `§`/`path:line` that this item's `contextRefs[]` points at **using Read** and explain it in plain language. Only explain — **do not pick the answer for the user**; after explaining, hand the decision back to the user.
|
|
87
|
-
3. **Free-form** — the user gives a narrative answer that matches neither a recommendation nor an alternative. `value` = that narrative verbatim, with the rationale in `rationale` if needed. `disposition: "answer"`.
|
|
88
|
-
4. **Hold + reframe** — if the user asks to "re-frame this question itself", record it as `disposition: "reframe"`. Put what the user wants re-asked into `value` verbatim. A reframe is not an answer, so it does not satisfy the approval gate — the next run re-frames that question.
|
|
136
|
+
If the user replies with a question instead of an answer ("what does this mean?"), record nothing: **Read** the `§`/`path:line` from `contextRefs[]`, explain it in plain language, and re-ask the same item with the same four options. Explain only — **do not resolve it for them**; the decision goes back to the user.
|
|
89
137
|
|
|
90
|
-
|
|
138
|
+
Echo one line per finished item (`[2/5] C-014 → answer: 60s`), then ask the next one. Do not summarize the whole set until Step 4.
|
|
91
139
|
|
|
92
140
|
## Step 4: Echo the full sidecar back → explicit "confirmed" gate
|
|
93
141
|
|
|
94
|
-
Once every open item
|
|
142
|
+
Once the loop has covered every open item, **before** calling `write`, **echo back** everything collected (each `id`, `disposition`, `value`, `rationale` if present, and the approval decision from Step 5 below) to the user as-is. Then ask for explicit confirmation:
|
|
95
143
|
|
|
96
144
|
> Record it as shown above? (Reply `confirmed` to write the sidecar.)
|
|
97
145
|
|
|
98
146
|
- **Never call `write`** until the user says `confirmed` (or gives clear approval).
|
|
99
147
|
- If the user changes any item, show the echo-back again with the changed value and re-confirm.
|
|
100
|
-
- Every `value` must be the user
|
|
148
|
+
- Every `value` must be what the user picked or typed, never a wording you settled on for them — this gate guarantees that.
|
|
101
149
|
|
|
102
150
|
## Step 5: (Optional) Record approval
|
|
103
151
|
|
|
@@ -166,7 +166,7 @@ def scan_forbidden_actions(
|
|
|
166
166
|
resolve_team_needles_with_source,
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
-
since, until = resolve_run_window(team_state_path, team_state)
|
|
169
|
+
since, until = resolve_run_window(team_state_path, team_state, relax_start=False)
|
|
170
170
|
lead_sid = (team_state.get("lead") or {}).get("sessionId") or ""
|
|
171
171
|
team_needles, _needle_source = resolve_team_needles_with_source(
|
|
172
172
|
team_state, project_root, since, until, projects_root=claude_projects_dir
|
|
@@ -3497,8 +3497,17 @@ def _validate_stage_carry_sidecar_exists(
|
|
|
3497
3497
|
stage = evidence.get("stageNumber")
|
|
3498
3498
|
if not isinstance(stage, int):
|
|
3499
3499
|
return
|
|
3500
|
-
#
|
|
3501
|
-
|
|
3500
|
+
# Carry sidecars are stage-SHARED: they live flat at
|
|
3501
|
+
# `runs/implementation/carry/stage-<N>.json`, NOT under the stage-<N>/ run
|
|
3502
|
+
# dir, because the next stage's carry-in and `consumers.backfill_done_from_carry`
|
|
3503
|
+
# glob them without knowing the producing run's layout (storage-model.md
|
|
3504
|
+
# §"representative files"). `report_path` is
|
|
3505
|
+
# `runs/implementation/stage-<N>/reports/final-report-...md`, so drop the
|
|
3506
|
+
# stage-<N>/ segment to reach the flat carry dir that `consumers` reads —
|
|
3507
|
+
# resolving it under the stage run dir forced the lead to write the file twice.
|
|
3508
|
+
run_dir = report_path.parent.parent
|
|
3509
|
+
carry_root = run_dir.parent if _stage_isolated_name(run_dir) else run_dir
|
|
3510
|
+
carry_path = carry_root / "carry" / f"stage-{stage}.json"
|
|
3502
3511
|
if not carry_path.exists():
|
|
3503
3512
|
failures.append(
|
|
3504
3513
|
f"implementation run declares stage-{stage} sidecar evidence but "
|
|
@@ -194,7 +194,7 @@ def _collect_lead_evidence(
|
|
|
194
194
|
)
|
|
195
195
|
from okstra_token_usage.paths import claude_project_dir
|
|
196
196
|
|
|
197
|
-
since, until = resolve_run_window(team_state_path, team_state)
|
|
197
|
+
since, until = resolve_run_window(team_state_path, team_state, relax_start=False)
|
|
198
198
|
lead_sid = (team_state.get("lead") or {}).get("sessionId") or ""
|
|
199
199
|
team_needles, _needle_source = resolve_team_needles_with_source(
|
|
200
200
|
team_state, project_root, since, until, projects_root=projects_root
|