okstra 0.118.0 → 0.120.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.md +2 -2
- package/docs/cli.md +4 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/globals.sh +1 -1
- package/runtime/bin/lib/okstra/usage.sh +2 -2
- package/runtime/bin/okstra-render-report-views.py +5 -35
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/python/okstra_ctl/clarification_items.py +48 -0
- package/runtime/python/okstra_ctl/models.py +1 -0
- package/runtime/python/okstra_ctl/report_views.py +46 -6
- package/runtime/python/okstra_ctl/run.py +1 -1
- package/runtime/python/okstra_ctl/user_response.py +190 -0
- package/runtime/python/okstra_ctl/wizard.py +1 -1
- package/runtime/python/okstra_token_usage/pricing.py +4 -0
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +121 -0
- package/runtime/templates/reports/report.js +6 -3
- package/runtime/templates/reports/user-response.template.md +1 -0
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/user-response.mjs +26 -0
- package/src/lib/skill-catalog.mjs +1 -0
package/docs/architecture.md
CHANGED
|
@@ -254,7 +254,7 @@ The standard `okstra` workflow applies the following team contract consistently
|
|
|
254
254
|
- The main Claude is always the `Claude lead` and operates in synthesis-only mode.
|
|
255
255
|
- The default required worker roles are `Claude worker`, `Codex worker`, and `Report writer worker`. `Antigravity worker` is optional and is included as required only when explicitly named by `--workers` or the profile's `- Workers:` section.
|
|
256
256
|
- `Report writer worker` focuses on report structure and evidence organization, but `Claude lead` remains the final synthesis owner.
|
|
257
|
-
- The default model contract is computed from central defaults. Fallbacks are `Claude lead`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.
|
|
257
|
+
- The default model contract is computed from central defaults. Fallbacks are `Claude lead`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.6`, and `Antigravity worker`=`auto` (when opted in). Without a separate override, `Report writer worker` follows the `Claude lead` model (therefore `opus` by default).
|
|
258
258
|
- Because `Antigravity worker` is optional, it is attempted only in runs where it is explicitly included.
|
|
259
259
|
- Before the final judgment, each required role in the current run's worker roster must have either a result or an explicit terminal status (`completed`, `timeout`, `error`, `not-run`).
|
|
260
260
|
- Every attempted worker (`completed`, `timeout`, `error`) must have an assigned worker prompt history file under the current run's `prompts/` directory.
|
|
@@ -741,7 +741,7 @@ Each validator blocks the phase with a `contract-violated` exit code when a cont
|
|
|
741
741
|
- The run directory is organized into typed subdirectories such as `manifests/`, `state/`, `prompts/`, `reports/`, `status/`, `sessions/`, and `worker-results/`; prompt snapshots are prepared under `prompts/` first.
|
|
742
742
|
- Claude creates workers and collects results.
|
|
743
743
|
- The standard workflow uses a `Claude lead` with default workers `Claude worker`, `Codex worker`, and `Report writer worker`; `Antigravity worker` is optional and included only when explicitly requested.
|
|
744
|
-
- Worker models can be overridden with `--lead-model`, `--claude-model`, `--codex-model`, `--antigravity-model`, and `--report-writer-model`; defaults are centrally managed through `OKSTRA_DEFAULT_*` environment variables. Fallback defaults are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.
|
|
744
|
+
- Worker models can be overridden with `--lead-model`, `--claude-model`, `--codex-model`, `--antigravity-model`, and `--report-writer-model`; defaults are centrally managed through `OKSTRA_DEFAULT_*` environment variables. Fallback defaults are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.6`, and `Antigravity worker`=`auto`.
|
|
745
745
|
- For `--task-type implementation`, select the provider that takes the Executor role with `--executor <claude|codex|antigravity>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`). Only the Executor may mutate project files. The other two providers and the Executor's own provider are each dispatched as verifiers in separate CLI sessions (session isolation preserves the self-review safeguard). The Executor's model reuses the selected provider's worker-model flag (`--claude-model` / `--codex-model` / `--antigravity-model`). Provider / displayName / workerAgent / model are recorded in the run-manifest `teamContract.executor` block.
|
|
746
746
|
- Worktree cwd injection by Executor: Codex / Antigravity executors pin cwd to the worktree at the CLI layer through wrappers (`okstra-codex-exec.sh -C` / `okstra-antigravity-exec.sh --include-directories`). Because the Bash tool has no per-call cwd argument, the Claude executor prefixes cwd-sensitive toolchain invocations (`cargo`/`npm`/`pnpm`/`bun`/`pytest`/`make`/`go`) with `cd {{EXECUTOR_WORKTREE_PATH}} && <cmd>` in the same Bash invocation. Wrapping in `bash -lc`/`bash -c` is prohibited because it hides the leading `cd` token and defeats permission auto-allow. Prefer working-directory flags such as `git -C` or `cargo --manifest-path` when available. See the *Executor Worktree* block in `prompts/profiles/implementation.md` and the Executor exception in `agents/workers/claude-worker.md` for details.
|
|
747
747
|
- The project-level current-task convenience pointer is `.okstra/discovery/latest-task.json`.
|
package/docs/cli.md
CHANGED
|
@@ -342,7 +342,7 @@ The Codex worker (`--workers codex`, `--codex-model`) and Codex lead runtime are
|
|
|
342
342
|
|
|
343
343
|
> 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:
|
|
344
344
|
> - 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`
|
|
345
|
-
> - Codex (`--codex-model`): `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `codex-auto-review`
|
|
345
|
+
> - Codex (`--codex-model`): `gpt-5.6`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, `codex-auto-review`
|
|
346
346
|
> - 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.
|
|
347
347
|
|
|
348
348
|
### `--claude-model`
|
|
@@ -358,7 +358,7 @@ When omitted, it uses the central default `OKSTRA_DEFAULT_LEAD_MODEL`, falling b
|
|
|
358
358
|
### `--codex-model`
|
|
359
359
|
|
|
360
360
|
Selects the model used by the `Codex worker`.
|
|
361
|
-
When omitted, it uses the central default `OKSTRA_DEFAULT_CODEX_MODEL`, falling back to `gpt-5.
|
|
361
|
+
When omitted, it uses the central default `OKSTRA_DEFAULT_CODEX_MODEL`, falling back to `gpt-5.6`.
|
|
362
362
|
|
|
363
363
|
### `--antigravity-model`
|
|
364
364
|
|
|
@@ -384,7 +384,7 @@ Fallback defaults are:
|
|
|
384
384
|
- `Claude lead`: `opus`
|
|
385
385
|
- `Report writer worker`: `opus`
|
|
386
386
|
- `Claude worker`: `opus`
|
|
387
|
-
- `Codex worker`: `gpt-5.
|
|
387
|
+
- `Codex worker`: `gpt-5.6`
|
|
388
388
|
- `Antigravity worker`: `auto`
|
|
389
389
|
- Implementation executor: `claude`, so the default is `Claude executor`.
|
|
390
390
|
|
|
@@ -394,7 +394,7 @@ Selects the provider that performs the Executor role for `--task-type implementa
|
|
|
394
394
|
|
|
395
395
|
- Default: `OKSTRA_DEFAULT_EXECUTOR` → fallback `claude`.
|
|
396
396
|
- The Executor is the **only worker allowed to mutate project files** in this run. The other two providers are dispatched as strict read-only verifiers in the same run.
|
|
397
|
-
- The Executor reuses the provider's worker model flag. With `--executor codex`, its model comes from `--codex-model`, default `gpt-5.
|
|
397
|
+
- The Executor reuses the provider's worker model flag. With `--executor codex`, its model comes from `--codex-model`, default `gpt-5.6`; with `--executor antigravity`, it comes from `--antigravity-model`, default `auto`.
|
|
398
398
|
- All three Claude, Codex, and Antigravity verifiers are always dispatched regardless of the Executor provider. Even the verifier using the same provider runs in a separate CLI session with isolated context, preserving the self-review safeguard.
|
|
399
399
|
- Codex and Antigravity mutate files through each CLI's auto-edit mode, for example `codex exec --sandbox workspace-write`, without passing through Claude-side Edit/Write tools. Mutations occur in the task worktree described below. Both wrappers—`scripts/okstra-codex-exec.sh` and `scripts/okstra-antigravity-exec.sh`—receive the worktree path as their fourth positional argument and forward it through `--add-dir` for Codex or `--include-directories` for Antigravity. Without it, the Codex `workspace-write` sandbox rejects worktree writes with EPERM.
|
|
400
400
|
- **Claude Executor cwd handling**: Claude's Bash tool has no per-call cwd argument and inherits the lead session cwd. To run cwd-sensitive toolchains such as `cargo`, `npm`, `pnpm`, `bun`, `pytest`, `make`, or `go` inside the worktree, prefix the invocation with `cd {{EXECUTOR_WORKTREE_PATH}} && <cmd>`. Keep `cd` as the leading token in a single Bash call so Claude Code permission auto-allow works; do not wrap it in `bash -lc "..."` or `bash -c "..."`, which hides `cd` and causes a permission prompt on every call. Prefer a tool's working-directory option—such as `git -C <path>`, `cargo --manifest-path`, or `pytest --rootdir`—over a `cd && ` chain. Edit/Write/Read tools already use absolute paths and need no cwd handling. This rule applies only to the Claude Executor; the Codex and Antigravity wrappers inject cwd.
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -161,7 +161,7 @@ REPORT_WRITER_MODEL_EXECUTION_VALUE=""
|
|
|
161
161
|
DEFAULT_WORKERS="claude,codex,report-writer"
|
|
162
162
|
DEFAULT_LEAD_MODEL_NAME="${OKSTRA_DEFAULT_LEAD_MODEL:-opus}"
|
|
163
163
|
DEFAULT_CLAUDE_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CLAUDE_MODEL:-opus}"
|
|
164
|
-
DEFAULT_CODEX_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CODEX_MODEL:-gpt-5.
|
|
164
|
+
DEFAULT_CODEX_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CODEX_MODEL:-gpt-5.6}"
|
|
165
165
|
DEFAULT_ANTIGRAVITY_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_ANTIGRAVITY_MODEL:-gemini-3.1-pro}"
|
|
166
166
|
DEFAULT_REPORT_WRITER_MODEL_NAME="${OKSTRA_DEFAULT_REPORT_WRITER_MODEL:-$DEFAULT_LEAD_MODEL_NAME}"
|
|
167
167
|
DISPLAY_COMMAND_NAME="${OKSTRA_COMMAND_NAME:-$(basename "$0")}"
|
|
@@ -90,7 +90,7 @@ options:
|
|
|
90
90
|
(Antigravity worker is optional; add \`antigravity\` explicitly, e.g. --workers claude,codex,antigravity,report-writer)
|
|
91
91
|
--lead-model Model for Claude lead. Default: OKSTRA_DEFAULT_LEAD_MODEL or opus
|
|
92
92
|
--claude-model Model for Claude worker. Default: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
|
|
93
|
-
--codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.
|
|
93
|
+
--codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.6
|
|
94
94
|
--antigravity-model Model for Antigravity worker. Default: OKSTRA_DEFAULT_ANTIGRAVITY_MODEL or auto
|
|
95
95
|
--report-writer-model
|
|
96
96
|
Model for report writer worker. Default: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or lead model default
|
|
@@ -124,7 +124,7 @@ model defaults:
|
|
|
124
124
|
Claude lead: OKSTRA_DEFAULT_LEAD_MODEL or opus
|
|
125
125
|
Report writer worker: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or Claude lead default
|
|
126
126
|
Claude worker: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
|
|
127
|
-
Codex worker: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.
|
|
127
|
+
Codex worker: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.6
|
|
128
128
|
Antigravity worker: OKSTRA_DEFAULT_ANTIGRAVITY_MODEL or auto
|
|
129
129
|
Implementation executor: OKSTRA_DEFAULT_EXECUTOR or claude (one of: claude | codex | antigravity)
|
|
130
130
|
|
|
@@ -23,7 +23,6 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
import argparse
|
|
25
25
|
import os
|
|
26
|
-
import re
|
|
27
26
|
import sys
|
|
28
27
|
from pathlib import Path
|
|
29
28
|
|
|
@@ -43,7 +42,7 @@ if (SCRIPTS_DIR / "okstra_ctl" / "report_views.py").is_file():
|
|
|
43
42
|
elif HOME_LIB.is_dir() and str(HOME_LIB) not in sys.path:
|
|
44
43
|
sys.path.insert(0, str(HOME_LIB))
|
|
45
44
|
|
|
46
|
-
from okstra_ctl.report_views import
|
|
45
|
+
from okstra_ctl.report_views import infer_run_meta, render_html_view # noqa: E402
|
|
47
46
|
|
|
48
47
|
|
|
49
48
|
_OKSTRA_HOME = Path(os.environ.get("OKSTRA_HOME", str(Path.home() / ".okstra")))
|
|
@@ -60,15 +59,6 @@ _TEMPLATES_DIRS = (
|
|
|
60
59
|
_OKSTRA_HOME / "templates" / "reports",
|
|
61
60
|
)
|
|
62
61
|
|
|
63
|
-
# task-type itself can contain hyphens (``implementation-planning``,
|
|
64
|
-
# ``final-verification``, ``release-handoff``), so the filename segment
|
|
65
|
-
# between ``final-report-`` and ``-<seq>.md`` is matched greedily; the
|
|
66
|
-
# trailing ``-(?P<seq>\d+)\.md$`` anchor pins seq to the last numeric tail.
|
|
67
|
-
_PATH_RE = re.compile(
|
|
68
|
-
r"runs/(?P<task_type>[^/]+)/reports/final-report-.+-(?P<seq>\d+)\.md$"
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
62
|
def _load_assets() -> tuple[str, str]:
|
|
73
63
|
css_text: str | None = None
|
|
74
64
|
js_text: str | None = None
|
|
@@ -87,23 +77,6 @@ def _load_assets() -> tuple[str, str]:
|
|
|
87
77
|
return css_text, js_text
|
|
88
78
|
|
|
89
79
|
|
|
90
|
-
def _infer_from_path(path: Path) -> dict[str, str]:
|
|
91
|
-
posix = path.as_posix()
|
|
92
|
-
m = _PATH_RE.search(posix)
|
|
93
|
-
if m:
|
|
94
|
-
return {"task_type": m.group("task_type"), "seq": m.group("seq")}
|
|
95
|
-
return {}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _infer_from_body(text: str) -> dict[str, str]:
|
|
99
|
-
found: dict[str, str] = {}
|
|
100
|
-
for label, key in (("Task Key", "task_key"), ("Task Type", "task_type")):
|
|
101
|
-
m = re.search(rf"^- {label}:\s*(\S.*?)\s*$", text, re.MULTILINE)
|
|
102
|
-
if m:
|
|
103
|
-
found[key] = m.group(1)
|
|
104
|
-
return found
|
|
105
|
-
|
|
106
|
-
|
|
107
80
|
def main(argv: list[str] | None = None) -> int:
|
|
108
81
|
parser = argparse.ArgumentParser(
|
|
109
82
|
description="Render the self-contained HTML view of an okstra final-report."
|
|
@@ -119,14 +92,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
119
92
|
if not report_path.is_file():
|
|
120
93
|
parser.error(f"final-report not found: {report_path}")
|
|
121
94
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
source_report = args.source_report or report_path.name
|
|
127
|
-
|
|
95
|
+
meta = infer_run_meta(
|
|
96
|
+
report_path, task_key=args.task_key, task_type=args.task_type,
|
|
97
|
+
seq=args.seq, source_report=args.source_report,
|
|
98
|
+
)
|
|
128
99
|
css, js = _load_assets()
|
|
129
|
-
meta = RunMeta(task_key=task_key, task_type=task_type, seq=seq, source_report=source_report)
|
|
130
100
|
html_path = render_html_view(report_path, run_meta=meta, css=css, js=js)
|
|
131
101
|
if html_path is None:
|
|
132
102
|
print("html: skipped (no §1 clarification rows — html view carries no interactive forms for this report)")
|
|
@@ -85,6 +85,7 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
85
85
|
- Source path: `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}`
|
|
86
86
|
- If the source path above is empty, no prior clarification response was attached to this run.
|
|
87
87
|
- If the source path is set, a copy is staged at `{{INSTRUCTION_SET_RELATIVE_PATH}}/clarification-response.md`. Read it before running workers; reconcile each `C-*` row in section 1 (`## 1. Clarification Items`) of the prior report against new evidence and record the outcome in the conditional `## 0. Clarification Response Carried In From Previous Run` section of this run's final report (render that heading only when carry-in is non-empty — the validator fails empty Section 0 stubs).
|
|
88
|
+
- When a `## <C-id>` block carries `- Disposition: reframe`, it is not an answer but a request to re-question — the user is asking you to redefine this item. Do not treat it as answered; reconstruct the question itself in a fresh `## 1` Clarification Item, and never let a `reframe` item satisfy the approval gate.
|
|
88
89
|
|
|
89
90
|
### Incremental re-verification (implementation-planning clarification re-runs only)
|
|
90
91
|
|
|
@@ -292,7 +292,7 @@ Assigned worker prompt history path: <Project Root>/<Prompt History Path>
|
|
|
292
292
|
2. `team-state-<task-type>-<seq>.json` → `workers[].usage.cliModel` for that role (initial run's actual execution value)
|
|
293
293
|
3. The `**Model:**` line of the initial Phase 4 prompt for that role (read from its persisted prompt-history file)
|
|
294
294
|
|
|
295
|
-
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess, do NOT fall back to training-data defaults — for codex this silently produces `o4-mini` instead of the assigned `gpt-5.
|
|
295
|
+
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess, do NOT fall back to training-data defaults — for codex this silently produces `o4-mini` instead of the assigned `gpt-5.6`-class model.
|
|
296
296
|
|
|
297
297
|
For Codex/Antigravity wrapper subagents, the `**Model:** <role>, <modelExecutionValue>` line is what their wrapper extracts to pass into the underlying CLI's `--model` flag. Omitting it forces the wrapper to fall back to its own training-data knowledge of the CLI's historical default.
|
|
298
298
|
|
|
@@ -126,7 +126,7 @@ The table below documents those prep-time seed values **for reference only** —
|
|
|
126
126
|
| Claude lead | opus | -- | orchestration + convergence supervision + final-report review/approval |
|
|
127
127
|
| Report writer worker | opus | report-writer-worker | `agents/workers/report-writer-worker.md` |
|
|
128
128
|
| Claude worker | opus | claude-worker | `agents/workers/claude-worker.md` |
|
|
129
|
-
| Codex worker | gpt-5.
|
|
129
|
+
| Codex worker | gpt-5.6 | codex-worker | generated from `agents/workers/_cli-wrapper-template.md` + `codex-worker.params.json` |
|
|
130
130
|
| Antigravity worker | auto | antigravity-worker | generated from `agents/workers/_cli-wrapper-template.md` + `antigravity-worker.params.json` |
|
|
131
131
|
|
|
132
132
|
All three analysis workers use dedicated agent definitions; Codex/Antigravity wrappers handle external CLI invocation internally; Claude worker runs as an in-process subagent with explicitly registered MCP tools so it does not fall back to `claude --mcp-cli` Bash invocations.
|
|
@@ -8,7 +8,7 @@ profile document.
|
|
|
8
8
|
- Team contract (shared):
|
|
9
9
|
- `Claude lead` is synthesis-only and stays distinct from `Claude worker` (or, in `implementation`, the `Executor` and verifiers).
|
|
10
10
|
- `Report writer worker` is the **author** of the final-report file; `Claude lead` reviews and approves the produced draft and does NOT write the file itself (see `team-contract` and `report-writer` for the authoritative contract).
|
|
11
|
-
- default model assignments are resolved from centralised defaults; the fallback values are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.
|
|
11
|
+
- default model assignments are resolved from centralised defaults; the fallback values are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.6`, `Antigravity worker`=`auto`. Phase-specific overrides (e.g. `implementation`'s executor binding) live in the per-profile document.
|
|
12
12
|
- every required worker listed in the per-profile `Required workers:` block must be attempted; the final verdict waits until each has either a result or an explicit terminal status (`timeout`, `error`, `not-run`).
|
|
13
13
|
- unnamed generic parallel workers must not replace the required role roster, and no additional sub-agent dispatch is allowed beyond this roster.
|
|
14
14
|
- Worker interaction model (shared — read before inferring behaviour from the roster):
|
|
@@ -12,7 +12,7 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
|
|
|
12
12
|
- **Verifier dispatch labelling.** Agent `name` = `<provider>-verifier` (here, and identically in `final-verification`); for a `codex` / `antigravity` verifier, Lead additionally injects `**Pane role:** verifier` into the dispatched prompt body. Naming/mapping rules and the token-attribution guarantee are owned by `prompts/lead/okstra-lead-contract.md` Phase 4 "Agent `name` on dispatch".
|
|
13
13
|
- The verifier slots are `Claude verifier` and `Codex verifier`, plus `Antigravity verifier` **only when `antigravity` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
|
|
14
14
|
- Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
|
|
15
|
-
- Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.
|
|
15
|
+
- Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.6`, `Antigravity verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.6`), antigravity→`--antigravity-model` (default `auto`).
|
|
16
16
|
- Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.
|
|
17
17
|
|
|
18
18
|
## Verifier QA duties (independent re-run mandate)
|
|
@@ -208,6 +208,54 @@ def parse_clarification_items(report_text: str) -> Optional[list[ClarificationIt
|
|
|
208
208
|
return _walk_section_1_table(section).items
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
def parse_clarification_rows(report_text: str) -> list[dict]:
|
|
212
|
+
"""§1 행마다 메타(ClarificationItem) + 원문 Statement / Expected form 셀.
|
|
213
|
+
|
|
214
|
+
``parse_clarification_items`` 와 같은 lenient 계약: §1 부재 또는 헤더
|
|
215
|
+
미인식이면 ``[]``. §1 테이블 셀 추출의 단일 참조점 — 다른 모듈이 §1
|
|
216
|
+
레이아웃을 다시 훑지 않도록 이 함수로 통일한다.
|
|
217
|
+
"""
|
|
218
|
+
section = _section_1_slice(report_text)
|
|
219
|
+
if section is None:
|
|
220
|
+
return []
|
|
221
|
+
lines = section.splitlines()
|
|
222
|
+
header_idx = -1
|
|
223
|
+
for idx, line in enumerate(lines):
|
|
224
|
+
if not line.lstrip().startswith("|"):
|
|
225
|
+
continue
|
|
226
|
+
cells = [c.lower() for c in _split_pipe_row(line)]
|
|
227
|
+
if "user input" in cells and any(c.startswith("statement") for c in cells):
|
|
228
|
+
header_idx = idx
|
|
229
|
+
break
|
|
230
|
+
if header_idx < 0:
|
|
231
|
+
return []
|
|
232
|
+
header = [c.lower() for c in _split_pipe_row(lines[header_idx])]
|
|
233
|
+
s_col = next((j for j, h in enumerate(header) if h.startswith("statement")), -1)
|
|
234
|
+
e_col = next((j for j, h in enumerate(header) if h.startswith("expected form")), -1)
|
|
235
|
+
rows: list[dict] = []
|
|
236
|
+
body = False
|
|
237
|
+
for line in lines[header_idx + 1:]:
|
|
238
|
+
if not line.lstrip().startswith("|"):
|
|
239
|
+
if body:
|
|
240
|
+
break
|
|
241
|
+
continue
|
|
242
|
+
if is_separator_row(line):
|
|
243
|
+
body = True
|
|
244
|
+
continue
|
|
245
|
+
if not body:
|
|
246
|
+
continue
|
|
247
|
+
cells = _split_pipe_row(line)
|
|
248
|
+
item = parse_meta_cell(cells[0]) if cells else None
|
|
249
|
+
if item is None:
|
|
250
|
+
continue
|
|
251
|
+
rows.append({
|
|
252
|
+
"item": item,
|
|
253
|
+
"statement": cells[s_col] if 0 <= s_col < len(cells) else "",
|
|
254
|
+
"expected_form": cells[e_col] if 0 <= e_col < len(cells) else "",
|
|
255
|
+
})
|
|
256
|
+
return rows
|
|
257
|
+
|
|
258
|
+
|
|
211
259
|
UNRESOLVED_STATUSES = {"open", "answered"}
|
|
212
260
|
|
|
213
261
|
|
|
@@ -36,6 +36,7 @@ ANTIGRAVITY_MAPPING = {
|
|
|
36
36
|
"gemini-3.5-flash": ("Gemini 3.5 Flash", "gemini-3.5-flash"),
|
|
37
37
|
}
|
|
38
38
|
CODEX_MAPPING = {
|
|
39
|
+
"gpt-5.6": ("gpt-5.6", "gpt-5.6"),
|
|
39
40
|
"gpt-5.5": ("gpt-5.5", "gpt-5.5"),
|
|
40
41
|
"gpt-5.4": ("gpt-5.4", "gpt-5.4"),
|
|
41
42
|
"gpt-5.4-mini": ("gpt-5.4-mini", "gpt-5.4-mini"),
|
|
@@ -108,6 +108,46 @@ class RunMeta:
|
|
|
108
108
|
source_report: str # relative path of the .md the HTML is derived from
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
# task-type itself can contain hyphens (``implementation-planning``,
|
|
112
|
+
# ``final-verification``, ``release-handoff``), so the filename segment
|
|
113
|
+
# between ``final-report-`` and ``-<seq>.md`` is matched greedily; the
|
|
114
|
+
# trailing ``-(?P<seq>\d+)\.md$`` anchor pins seq to the last numeric tail.
|
|
115
|
+
_REPORT_PATH_RE = re.compile(
|
|
116
|
+
r"runs/(?P<task_type>[^/]+)/reports/final-report-.+-(?P<seq>\d+)\.md$"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _infer_run_meta_from_path(path: Path) -> dict:
|
|
121
|
+
m = _REPORT_PATH_RE.search(path.as_posix())
|
|
122
|
+
return {"task_type": m.group("task_type"), "seq": m.group("seq")} if m else {}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _infer_run_meta_from_body(text: str) -> dict:
|
|
126
|
+
found: dict[str, str] = {}
|
|
127
|
+
for label, key in (("Task Key", "task_key"), ("Task Type", "task_type")):
|
|
128
|
+
m = re.search(rf"^- {label}:\s*(\S.*?)\s*$", text, re.MULTILINE)
|
|
129
|
+
if m:
|
|
130
|
+
found[key] = m.group(1)
|
|
131
|
+
return found
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def infer_run_meta(report_path: Path, *, task_key: Optional[str] = None,
|
|
135
|
+
task_type: Optional[str] = None, seq: Optional[str] = None,
|
|
136
|
+
source_report: Optional[str] = None) -> RunMeta:
|
|
137
|
+
"""Derive a ``RunMeta`` from a final-report path/body, honouring any
|
|
138
|
+
explicit override. Single reference point for BOTH the HTML view render
|
|
139
|
+
script and the in-session user-response writer, so sidecar match keys
|
|
140
|
+
(task_type/seq/source-report) never drift between the two paths."""
|
|
141
|
+
text = report_path.read_text(encoding="utf-8")
|
|
142
|
+
inferred = {**_infer_run_meta_from_path(report_path), **_infer_run_meta_from_body(text)}
|
|
143
|
+
return RunMeta(
|
|
144
|
+
task_key=task_key or inferred.get("task_key") or "unknown",
|
|
145
|
+
task_type=task_type or inferred.get("task_type") or "unknown",
|
|
146
|
+
seq=seq or inferred.get("seq") or "000",
|
|
147
|
+
source_report=source_report or report_path.name,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
111
151
|
@dataclass(frozen=True)
|
|
112
152
|
class ReportViewModel:
|
|
113
153
|
run_meta: RunMeta
|
|
@@ -924,6 +964,7 @@ class UserResponseEntry:
|
|
|
924
964
|
kind: str
|
|
925
965
|
value: str
|
|
926
966
|
rationale: Optional[str] = None
|
|
967
|
+
disposition: str = "answer"
|
|
927
968
|
|
|
928
969
|
|
|
929
970
|
@dataclass(frozen=True)
|
|
@@ -961,12 +1002,11 @@ def serialize_user_response(
|
|
|
961
1002
|
has_approval = approval is not None and approval.approved
|
|
962
1003
|
body_chunks: list[str] = []
|
|
963
1004
|
for e in entries:
|
|
964
|
-
chunk =
|
|
965
|
-
|
|
966
|
-
f"-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
)
|
|
1005
|
+
chunk = f"\n## {e.response_id}\n- Kind: {e.kind}\n"
|
|
1006
|
+
if e.disposition != "answer":
|
|
1007
|
+
chunk += f"- Disposition: {e.disposition}\n"
|
|
1008
|
+
value_lines = e.value.strip().split("\n")
|
|
1009
|
+
chunk += "- Value:\n" + "".join(f" > {ln}\n" for ln in value_lines)
|
|
970
1010
|
if e.rationale:
|
|
971
1011
|
chunk += f"- Rationale: {e.rationale.strip()}\n"
|
|
972
1012
|
body_chunks.append(chunk)
|
|
@@ -1343,7 +1343,7 @@ def recommended_role_models() -> dict[str, str]:
|
|
|
1343
1343
|
return {
|
|
1344
1344
|
"lead": lead_default,
|
|
1345
1345
|
"claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "opus"),
|
|
1346
|
-
"codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.
|
|
1346
|
+
"codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.6"),
|
|
1347
1347
|
"antigravity": _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", "auto"),
|
|
1348
1348
|
"report-writer": _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default),
|
|
1349
1349
|
}
|
|
@@ -8,10 +8,26 @@ the ``## APPROVAL`` block — the implementation wizard consumes it to offer
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import argparse
|
|
12
|
+
import datetime as dt
|
|
13
|
+
import json
|
|
11
14
|
import re
|
|
15
|
+
import sys
|
|
12
16
|
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
13
18
|
from typing import Optional
|
|
14
19
|
|
|
20
|
+
from okstra_ctl.report_views import (
|
|
21
|
+
serialize_user_response, UserResponseEntry, UserResponseApproval, infer_run_meta,
|
|
22
|
+
)
|
|
23
|
+
from okstra_ctl.report_view_artifacts import user_responses_dir_for_report
|
|
24
|
+
from okstra_ctl.listing import list_runs, absolute_final_report_path
|
|
25
|
+
from okstra_ctl.clarification_items import (
|
|
26
|
+
parse_clarification_rows,
|
|
27
|
+
scan_approval_gate,
|
|
28
|
+
section_1_present_but_unparsed,
|
|
29
|
+
)
|
|
30
|
+
|
|
15
31
|
_APPROVAL_HEADING_RE = re.compile(r"^## APPROVAL\s*$", re.MULTILINE)
|
|
16
32
|
_NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
|
|
17
33
|
|
|
@@ -53,3 +69,177 @@ def parse_user_response_approval(
|
|
|
53
69
|
source_report=rm.group(1) if rm else "",
|
|
54
70
|
seq=sm.group(1) if sm else "",
|
|
55
71
|
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_RESPONSE_HEADING_RE = re.compile(r"^## (?P<id>[A-Za-z][A-Za-z0-9]*-\d+)\s*$", re.MULTILINE)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _field(block: str, key: str) -> Optional[str]:
|
|
78
|
+
m = re.search(rf"^- {re.escape(key)}:\s*(\S.*?)\s*$", block, re.MULTILINE)
|
|
79
|
+
return m.group(1) if m else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _value(block: str) -> str:
|
|
83
|
+
# Value 는 "- Value:" 다음 줄들의 " > " 인용 블록.
|
|
84
|
+
m = re.search(r"^- Value:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE)
|
|
85
|
+
if not m:
|
|
86
|
+
return ""
|
|
87
|
+
lines = [re.sub(r"^\s*>\s?", "", ln) for ln in m.group(1).splitlines()]
|
|
88
|
+
return "\n".join(lines).strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_user_response_entries(sidecar_text: str) -> list[UserResponseEntry]:
|
|
92
|
+
"""Reverse of ``serialize_user_response`` for the per-response ``## C-*``
|
|
93
|
+
blocks. The ``## APPROVAL`` block is skipped (read separately by
|
|
94
|
+
``parse_user_response_approval``)."""
|
|
95
|
+
entries: list[UserResponseEntry] = []
|
|
96
|
+
matches = list(_RESPONSE_HEADING_RE.finditer(sidecar_text))
|
|
97
|
+
for i, m in enumerate(matches):
|
|
98
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(sidecar_text)
|
|
99
|
+
block = sidecar_text[m.end():end]
|
|
100
|
+
rationale = _field(block, "Rationale")
|
|
101
|
+
entries.append(UserResponseEntry(
|
|
102
|
+
response_id=m.group("id"),
|
|
103
|
+
kind=_field(block, "Kind") or "",
|
|
104
|
+
value=_value(block),
|
|
105
|
+
rationale=rationale,
|
|
106
|
+
disposition=_field(block, "Disposition") or "answer",
|
|
107
|
+
))
|
|
108
|
+
return entries
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_SEQ_FROM_REPORT_RE = re.compile(r"final-report-.+-(\w+)\.md$")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _seq_from_report(report: Path) -> str:
|
|
115
|
+
m = _SEQ_FROM_REPORT_RE.search(report.name)
|
|
116
|
+
return m.group(1) if m else ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def list_awaiting_tasks(home: Path, project_id: str, limit: int) -> list[dict]:
|
|
120
|
+
"""approval-open clarification 이 있는 태스크를 최신 report mtime 순으로."""
|
|
121
|
+
seen: set[str] = set()
|
|
122
|
+
out: list[dict] = []
|
|
123
|
+
for row in list_runs(home, project=project_id, limit=0): # startedAt desc
|
|
124
|
+
grp, tid = row.get("taskGroup", ""), row.get("taskId", "")
|
|
125
|
+
key = f"{grp}/{tid}" if grp and tid else ""
|
|
126
|
+
if not key or key in seen:
|
|
127
|
+
continue
|
|
128
|
+
seen.add(key)
|
|
129
|
+
report = absolute_final_report_path(row)
|
|
130
|
+
if report is None or not report.is_file():
|
|
131
|
+
continue
|
|
132
|
+
text = report.read_text(encoding="utf-8")
|
|
133
|
+
scan = scan_approval_gate(text)
|
|
134
|
+
base = {"taskKey": key, "taskType": row.get("taskType", ""),
|
|
135
|
+
"seq": _seq_from_report(report), "reportPath": str(report),
|
|
136
|
+
"reportMtime": report.stat().st_mtime}
|
|
137
|
+
if scan.unreadable_reason is not None:
|
|
138
|
+
# §1 heading exists but drifted → real warning; §1 simply absent → skip.
|
|
139
|
+
if section_1_present_but_unparsed(text):
|
|
140
|
+
out.append({**base, "openApprovalCount": 0, "unreadable": True})
|
|
141
|
+
continue
|
|
142
|
+
if not scan.blockers:
|
|
143
|
+
continue
|
|
144
|
+
out.append({**base, "openApprovalCount": len(scan.blockers), "unreadable": False})
|
|
145
|
+
out.sort(key=lambda t: t["reportMtime"], reverse=True)
|
|
146
|
+
return out[:limit] if limit > 0 else out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_SECTION_REF_RE = re.compile(r"§[\d.]+|C-\d+|[\w./-]+\.\w+:\d+")
|
|
150
|
+
_ALTERNATIVES_CUE = "Alternatives:"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _alternatives_from_expected(expected_form: str) -> list[str]:
|
|
154
|
+
idx = expected_form.find(_ALTERNATIVES_CUE)
|
|
155
|
+
if idx < 0:
|
|
156
|
+
return []
|
|
157
|
+
tail = expected_form[idx + len(_ALTERNATIVES_CUE):]
|
|
158
|
+
parts = re.split(r"[/;]", tail)
|
|
159
|
+
return [p.strip(" .,;—-") for p in parts if p.strip(" .,;—-")]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def show_open_rows(report_path: Path) -> dict:
|
|
163
|
+
text = report_path.read_text(encoding="utf-8")
|
|
164
|
+
rows = []
|
|
165
|
+
for r in parse_clarification_rows(text):
|
|
166
|
+
it = r["item"]
|
|
167
|
+
if it.status not in ("open", "answered"):
|
|
168
|
+
continue
|
|
169
|
+
statement, expected = r["statement"], r["expected_form"]
|
|
170
|
+
refs = sorted(set(_SECTION_REF_RE.findall(statement + " " + expected)))
|
|
171
|
+
rows.append({"id": it.row_id, "kind": it.kind, "blocks": it.blocks,
|
|
172
|
+
"status": it.status, "statement": statement,
|
|
173
|
+
"recommended": expected, # raw cell keeps answer + rationale
|
|
174
|
+
"alternatives": _alternatives_from_expected(expected),
|
|
175
|
+
"contextRefs": refs})
|
|
176
|
+
return {"reportPath": str(report_path), "rows": rows}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def write_sidecar(report_path: Path, answers: list[dict],
|
|
180
|
+
approval: Optional[dict], created_at: str,
|
|
181
|
+
task_key: str = "") -> Path:
|
|
182
|
+
run_meta = infer_run_meta(report_path, task_key=task_key or None)
|
|
183
|
+
out_dir = user_responses_dir_for_report(report_path)
|
|
184
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
sidecar = out_dir / f"user-response-{run_meta.task_type}-{run_meta.seq}.md"
|
|
186
|
+
|
|
187
|
+
merged: dict[str, UserResponseEntry] = {}
|
|
188
|
+
if sidecar.is_file():
|
|
189
|
+
for e in parse_user_response_entries(sidecar.read_text(encoding="utf-8")):
|
|
190
|
+
merged[e.response_id] = e
|
|
191
|
+
for a in answers:
|
|
192
|
+
merged[a["id"]] = UserResponseEntry(
|
|
193
|
+
response_id=a["id"], kind=a.get("kind", ""), value=a["value"],
|
|
194
|
+
rationale=a.get("rationale"), disposition=a.get("disposition", "answer"))
|
|
195
|
+
|
|
196
|
+
appr = None
|
|
197
|
+
if approval and approval.get("approved"):
|
|
198
|
+
appr = UserResponseApproval(
|
|
199
|
+
approved=True, implementation_option=approval.get("implementationOption", ""))
|
|
200
|
+
sidecar.write_text(
|
|
201
|
+
serialize_user_response(run_meta=run_meta, entries=list(merged.values()),
|
|
202
|
+
created_at=created_at, approval=appr),
|
|
203
|
+
encoding="utf-8")
|
|
204
|
+
return sidecar
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
208
|
+
parser = argparse.ArgumentParser(prog="okstra user-response")
|
|
209
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
210
|
+
|
|
211
|
+
pl = sub.add_parser("list")
|
|
212
|
+
pl.add_argument("--home", required=True)
|
|
213
|
+
pl.add_argument("--project", required=True)
|
|
214
|
+
pl.add_argument("--limit", type=int, default=3)
|
|
215
|
+
|
|
216
|
+
ps = sub.add_parser("show")
|
|
217
|
+
ps.add_argument("--report", required=True)
|
|
218
|
+
|
|
219
|
+
pw = sub.add_parser("write")
|
|
220
|
+
pw.add_argument("--report", required=True)
|
|
221
|
+
pw.add_argument("--answers", required=True, help="JSON array of answer entries")
|
|
222
|
+
pw.add_argument("--approval", default="", help="JSON approval object")
|
|
223
|
+
pw.add_argument("--task-key", default="", help="task-key from list/show context")
|
|
224
|
+
|
|
225
|
+
ns = parser.parse_args(argv)
|
|
226
|
+
if ns.cmd == "list":
|
|
227
|
+
out = list_awaiting_tasks(Path(ns.home), ns.project, ns.limit)
|
|
228
|
+
json.dump(out, sys.stdout, ensure_ascii=False)
|
|
229
|
+
return 0
|
|
230
|
+
if ns.cmd == "show":
|
|
231
|
+
json.dump(show_open_rows(Path(ns.report)), sys.stdout, ensure_ascii=False)
|
|
232
|
+
return 0
|
|
233
|
+
if ns.cmd == "write":
|
|
234
|
+
answers = json.loads(ns.answers)
|
|
235
|
+
approval = json.loads(ns.approval) if ns.approval else None
|
|
236
|
+
created_at = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
237
|
+
p = write_sidecar(Path(ns.report), answers, approval, created_at,
|
|
238
|
+
task_key=ns.task_key)
|
|
239
|
+
json.dump({"sidecar": str(p)}, sys.stdout, ensure_ascii=False)
|
|
240
|
+
return 0
|
|
241
|
+
return 1
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
raise SystemExit(main())
|
|
@@ -129,7 +129,7 @@ CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
|
129
129
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
130
130
|
|
|
131
131
|
CLAUDE_MODEL_OPTIONS = ["default", "fable", "opus", "sonnet", "haiku"]
|
|
132
|
-
CODEX_MODEL_OPTIONS = ["default", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]
|
|
132
|
+
CODEX_MODEL_OPTIONS = ["default", "gpt-5.6", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]
|
|
133
133
|
ANTIGRAVITY_MODEL_OPTIONS = ["default", "gemini-3.1-pro", "gemini-3.5-flash"]
|
|
134
134
|
|
|
135
135
|
# special pick value: start a brand-new task
|
|
@@ -74,6 +74,10 @@ CODEX_PRICING = {
|
|
|
74
74
|
# is set equal to input as a conservative no-discount default.
|
|
75
75
|
|
|
76
76
|
# GPT-5 series.
|
|
77
|
+
# gpt-5.6 pricing assumed equal to gpt-5.5 (its flagship predecessor) until
|
|
78
|
+
# OpenAI publishes rates; without this entry it prefix-matches base `gpt-5`
|
|
79
|
+
# ($1.25) and undercounts cost.
|
|
80
|
+
"gpt-5.6": (5.00, 0.50, 30.0),
|
|
77
81
|
"gpt-5.5": (5.00, 0.50, 30.0),
|
|
78
82
|
"gpt-5.4-mini": (0.75, 0.075, 4.50),
|
|
79
83
|
"gpt-5.4": (2.50, 0.25, 15.0),
|
|
@@ -777,7 +777,7 @@ okstra recap note <resolved-target> --project-root <projectRoot> \
|
|
|
777
777
|
**self-check 규칙 (validator 가 없으니 스스로 지킨다):**
|
|
778
778
|
|
|
779
779
|
1. **렌더된 report 를 수기 편집하지 않는다** (`runs/*/reports/` 아래 `*.md` / `*.data.json` / `*.html`). `data.json` 에서 재생성되므로 편집은 덮어써지고 SSOT 와 어긋난다. 발견 내용은 `notes/` 에 쓴다.
|
|
780
|
-
2. **`user-responses/` 파일을 작성하거나 `created-by: user` 를 달지 않는다.** 그건 사용자의 결정이며, 대신 쓰면 사용자가 내리지 않은 결정을 위조하는 것이다. 네 증거가 특정 답을 뒷받침하면 `notes/` 에 그렇게 적고 결정은 사용자에게 맡긴다.
|
|
780
|
+
2. **`user-responses/` 파일을 작성하거나 `created-by: user` 를 달지 않는다.** 그건 사용자의 결정이며, 대신 쓰면 사용자가 내리지 않은 결정을 위조하는 것이다. 네 증거가 특정 답을 뒷받침하면 `notes/` 에 그렇게 적고 결정은 사용자에게 맡긴다. 단, `okstra-user-response` 스킬의 에코백 확인 흐름은 예외다 — 그 흐름에서는 사용자가 세션에서 직접 결정을 내리고 CLI 는 전사 채널일 뿐이므로, `write` 로 `created-by: user` sidecar 를 기록하는 것이 정당하다. 이 예외는 사용자가 명시적으로 "맞음" 을 확인한 뒤에만, verbatim 입력에 대해서만 성립한다.
|
|
781
781
|
3. **okstra 관리 디렉토리에 쓰지 않는다** (`runs/`, `instruction-set/`, `history/`, `recap/`, `.okstra/decisions/`). `notes/` 만이 agent 소유 레인이다. `recap/recap-log.jsonl` 은 예외적으로 쓰되 손으로 편집하지 말고 `okstra recap record` CLI 로만 남긴다.
|
|
782
782
|
4. **`notes/` 는 okstra 에 inert 하다** — 어떤 run 도 자동으로 읽지 않는다. 작성 후 CLI 가 출력한 `clarificationResponseArg`(예: `--clarification-response <notePath>`)를 사용자에게 그대로 알려, 다음 run 을 그 인자와 함께 실행해야만 반영된다는 것을 전한다.
|
|
783
783
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: okstra-user-response
|
|
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, presents each open C-id (statement + recommended answer + alternatives), collects the user's own verbatim answers (or a reframe/hold), echoes the full sidecar 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 for the user — it only relays recommendations and transcribes what the user decides.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# OKSTRA User Response
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
**Core principle — the skill never picks an answer for the user.** This skill only *presents* recommendations and alternatives; every `value` is the user's own verbatim input. It never calls `write` until the user has explicitly confirmed.
|
|
12
|
+
|
|
13
|
+
| Sub-command | What it does |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `list` | List tasks that still have approval-open clarification, newest report first. |
|
|
16
|
+
| `show` | Expand one report's open `C-*` rows (statement + recommended + alternatives + contextRefs). |
|
|
17
|
+
| `write` | Record the collected answers (+ optional approval) as a `user-responses/` sidecar. |
|
|
18
|
+
|
|
19
|
+
## Step 0: Preflight (shared)
|
|
20
|
+
|
|
21
|
+
Before anything, run one Bash tool call whose leading token is the literal `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` — a non-literal leading token defeats the `Bash(okstra:*)` permission match):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
okstra preflight --runtime claude-code --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Branch on the stdout JSON:
|
|
28
|
+
- `ok: true` → carry `projectRoot` and `projectId` as literal strings; they are the base for every step below.
|
|
29
|
+
- `ok: false` → this project has no okstra setup. Tell the user: "this project has no okstra setup. Run `/okstra-setup` first." Then stop. If the user pointed at a specific project directory, re-run targeting it: `okstra preflight --runtime claude-code --cwd <that-dir> --json` (`--cwd` is the sanctioned way to target a project — a leading `cd` would break the permission match); only if that **also** returns `ok:false` do you stop. If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary).
|
|
30
|
+
|
|
31
|
+
Then resolve the okstra home once (the `list` sub-command needs it):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
okstra paths --field home
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Paste the printed path literally into `--home` below. Every subsequent `okstra <subcmd>` call self-bootstraps its Python path, so this skill never needs `okstra paths --shell` / `export PYTHONPATH=...`.
|
|
38
|
+
|
|
39
|
+
## Step 1: List awaiting tasks → picker
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
okstra user-response list --home <resolved-home> --project <projectId> --limit 3
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Returns a JSON array (latest report mtime first); each entry:
|
|
46
|
+
`{taskKey, taskType, seq, reportPath, reportMtime, openApprovalCount, unreadable}`.
|
|
47
|
+
|
|
48
|
+
- Empty array → answer `No task has open clarification items.` and stop.
|
|
49
|
+
- Otherwise present a **3-option picker**: the top recommendations from the list (each shown as `taskKey (taskType, N open items)`), and the **final option is always "Enter directly"** — where the user pastes a `reportPath` or a `task-key` directly (for a task not in the top-3 window).
|
|
50
|
+
- An entry with `unreadable: true` means its `## 1. Clarification Items` heading exists but drifted from the expected format. Flag it as `⚠ §1 format drift — the CLI could not parse its items` and do not proceed on it until the report is regenerated; do not fabricate rows for it.
|
|
51
|
+
|
|
52
|
+
Carry the chosen entry's `reportPath` and `taskKey` forward.
|
|
53
|
+
|
|
54
|
+
## Step 2: Show open rows
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
okstra user-response show --report <reportPath>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Returns `{reportPath, rows: [{id, kind, blocks, status, statement, recommended, alternatives, contextRefs}]}`. For each open `C-*` row, present to the user:
|
|
61
|
+
|
|
62
|
+
- **`id`** and its **`statement`** (the original question).
|
|
63
|
+
- **`recommended`** — the answer okstra proposed (+ rationale). It is only a recommendation.
|
|
64
|
+
- **`alternatives[]`** — list them if present.
|
|
65
|
+
- **`contextRefs[]`** — the report `§`/`C-id`/`path:line` references this question points at.
|
|
66
|
+
|
|
67
|
+
## Step 3: Per-item — collect the user's decision (4 branches)
|
|
68
|
+
|
|
69
|
+
For each open item, the user picks one of four dispositions. You relay and transcribe; you never choose:
|
|
70
|
+
|
|
71
|
+
1. **Answer** — the user accepts the recommendation or gives their own answer. `disposition: "answer"`, `value` = the user's utterance verbatim.
|
|
72
|
+
2. **Ask-to-explain** — if the user asks "what does this mean?", 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.
|
|
73
|
+
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"`.
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
Each answer item's JSON shape: `{id, kind, value, rationale?, disposition}`. `kind` uses the same `kind` that `show` gave for that row.
|
|
77
|
+
|
|
78
|
+
## Step 4: Echo the full sidecar back → explicit "confirmed" gate
|
|
79
|
+
|
|
80
|
+
Once every open item is handled, **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:
|
|
81
|
+
|
|
82
|
+
> Record it as shown above? (Reply `confirmed` to write the sidecar.)
|
|
83
|
+
|
|
84
|
+
- **Never call `write`** until the user says `confirmed` (or gives clear approval).
|
|
85
|
+
- If the user changes any item, show the echo-back again with the changed value and re-confirm.
|
|
86
|
+
- Every `value` must be the user's utterance verbatim — this gate guarantees that.
|
|
87
|
+
|
|
88
|
+
## Step 5: (Optional) Record approval
|
|
89
|
+
|
|
90
|
+
Record approval alongside only when the report's approval-blocking clarification items are **all filled with an answer** and the user explicitly said "approve with this option":
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
--approval '{"approved":true,"implementationOption":"<the option the user chose>"}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If any approval-target item is not filled with an answer, or is a reframe/hold, do not approve — tell the user the gate is still open.
|
|
97
|
+
|
|
98
|
+
## Step 6: Write the sidecar
|
|
99
|
+
|
|
100
|
+
Run only after the user has confirmed the echo-back with "confirmed":
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
okstra user-response write --report <reportPath> --answers '<answers-json>' --approval '<approval-json>' --task-key <taskKey>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `--answers` = the JSON array of `{id, kind, value, rationale?, disposition}` objects collected in Step 3.
|
|
107
|
+
- `--approval` is attached only when recording approval in Step 5 (otherwise omit it).
|
|
108
|
+
- `--task-key` is the value carried forward from Step 1/2.
|
|
109
|
+
- Report the path from the returned JSON `{sidecar: <path>}` to the user. (`write` merges items when a sidecar already exists — the same `id` is overwritten with the new value.)
|
|
110
|
+
|
|
111
|
+
## Step 7: Self-describing next-step guidance
|
|
112
|
+
|
|
113
|
+
Leave the following in the final output as-is:
|
|
114
|
+
|
|
115
|
+
> This answer was recorded in the `user-responses/` sidecar (`<sidecar path>`). Re-running this task's `<task-type>` with `/okstra-run` auto-attaches this answer (if you approved all approval-blocking items, the next phase is `implementation`).
|
|
116
|
+
|
|
117
|
+
## Output Rules (shared)
|
|
118
|
+
|
|
119
|
+
- Keep responses concise and in the language the user is using, unless they ask otherwise.
|
|
120
|
+
- Give path guidance host-relative (`~/.okstra/...` or relative to the `projectRoot` preflight returned). Use repo paths only when pointing at a code source.
|
|
121
|
+
- This skill never hand-edits a rendered report (`runs/*/reports/*.md` / `*.data.json`). Writing the `user-responses/` sidecar goes only through the `okstra user-response write` CLI.
|
|
@@ -144,9 +144,12 @@
|
|
|
144
144
|
var e = entries[i];
|
|
145
145
|
var chunk =
|
|
146
146
|
"\n## " + e.responseId + "\n" +
|
|
147
|
-
"- Kind: " + e.kind + "\n"
|
|
148
|
-
|
|
149
|
-
"
|
|
147
|
+
"- Kind: " + e.kind + "\n";
|
|
148
|
+
if (e.disposition && e.disposition !== "answer") {
|
|
149
|
+
chunk += "- Disposition: " + e.disposition + "\n";
|
|
150
|
+
}
|
|
151
|
+
var valueLines = trimMultiline(e.value).split("\n");
|
|
152
|
+
chunk += "- Value:\n" + valueLines.map(function (ln) { return " > " + ln + "\n"; }).join("");
|
|
150
153
|
if (e.rationale) {
|
|
151
154
|
chunk += "- Rationale: " + trimMultiline(e.rationale) + "\n";
|
|
152
155
|
}
|
|
@@ -25,6 +25,7 @@ created-at: <ISO 8601 UTC timestamp>
|
|
|
25
25
|
```markdown
|
|
26
26
|
## <Response ID>
|
|
27
27
|
- Kind: <material | decision | data-point>
|
|
28
|
+
- Disposition: <answer | reframe> # answer(기본)면 라인 생략. reframe = 답 보류 + 재질문(행 미해결 유지)
|
|
28
29
|
- Value:
|
|
29
30
|
> <multi-line value, trimmed>
|
|
30
31
|
- Rationale: <optional one-line rationale>
|
package/src/cli-registry.mjs
CHANGED
|
@@ -166,6 +166,13 @@ export const COMMAND_REGISTRY = [
|
|
|
166
166
|
category: "introspection",
|
|
167
167
|
summary: ["Assemble a task's run-to-run phase recap or append a recap log entry"],
|
|
168
168
|
},
|
|
169
|
+
{
|
|
170
|
+
name: "user-response",
|
|
171
|
+
module: "./commands/inspect/user-response.mjs",
|
|
172
|
+
export: "run",
|
|
173
|
+
category: "introspection",
|
|
174
|
+
summary: ["Answer a task's open clarification questions in-session and write the response sidecar"],
|
|
175
|
+
},
|
|
169
176
|
{
|
|
170
177
|
name: "rollup",
|
|
171
178
|
module: "./commands/inspect/rollup.mjs",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra user-response — 태스크의 열린 clarification 질문에 세션 내에서 답하고 sidecar 기록
|
|
4
|
+
|
|
5
|
+
A thin shim over \`python3 -m okstra_ctl.user_response\`. JSON 출력.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
okstra user-response list --home <dir> --project <id> [--limit <n>]
|
|
9
|
+
okstra user-response show --report <md>
|
|
10
|
+
okstra user-response write --report <md> --answers <json> \\
|
|
11
|
+
[--approval <json>] [--task-key <key>]
|
|
12
|
+
|
|
13
|
+
Exit codes: 0 ok / 1 오류
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function run(args) {
|
|
17
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
18
|
+
process.stdout.write(USAGE);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const { code } = await runPythonModule({
|
|
22
|
+
module: "okstra_ctl.user_response",
|
|
23
|
+
args,
|
|
24
|
+
});
|
|
25
|
+
return code ?? 1;
|
|
26
|
+
}
|