okstra 0.184.0 → 0.185.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +2 -1
  2. package/dist/cli-registry.mjs +9 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/chat/chat.d.mts +1 -0
  5. package/dist/commands/chat/chat.mjs +386 -0
  6. package/dist/commands/chat/chat.mjs.map +1 -0
  7. package/dist/lib/skill-catalog.mjs +1 -0
  8. package/dist/lib/skill-catalog.mjs.map +1 -1
  9. package/docs/architecture.md +9 -7
  10. package/docs/cli.md +3 -2
  11. package/docs/for-ai/README.md +4 -2
  12. package/docs/for-ai/skills/okstra-chat.md +34 -0
  13. package/docs/for-ai/skills/okstra-inspect.md +1 -1
  14. package/docs/for-ai/skills/okstra-run.md +2 -2
  15. package/docs/for-ai/skills/okstra-user-response.md +10 -8
  16. package/docs/project-structure-overview.md +6 -5
  17. package/docs/task-process/README.md +1 -1
  18. package/docs/task-process/implementation-planning.md +1 -1
  19. package/package.json +1 -1
  20. package/runtime/BUILD.json +2 -2
  21. package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
  22. package/runtime/prompts/lead/plan-body-verification.md +8 -7
  23. package/runtime/prompts/lead/report-writer.md +1 -1
  24. package/runtime/prompts/profiles/_clarification-recommendation.md +2 -2
  25. package/runtime/prompts/profiles/implementation-planning.md +4 -3
  26. package/runtime/prompts/wizard/prompts.ko.json +2 -0
  27. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +2 -2
  28. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  29. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
  30. package/runtime/python/okstra_ctl/incremental_carry.py +149 -5
  31. package/runtime/python/okstra_ctl/incremental_scope.py +25 -3
  32. package/runtime/python/okstra_ctl/next_phase.py +67 -4
  33. package/runtime/python/okstra_ctl/plan_items.py +40 -4
  34. package/runtime/python/okstra_ctl/user_response.py +147 -37
  35. package/runtime/python/okstra_ctl/wizard.py +13 -0
  36. package/runtime/skills/okstra-chat/SKILL.md +108 -0
  37. package/runtime/skills/okstra-inspect/facets/status.md +6 -5
  38. package/runtime/skills/okstra-run/SKILL.md +2 -2
  39. package/runtime/skills/okstra-user-response/SKILL.md +50 -16
  40. package/runtime/validators/validate-run.py +109 -34
@@ -2733,6 +2733,8 @@ def _build_task_type(state: WizardState) -> Prompt:
2733
2733
  recommended_suffix = t["options"].get("_RECOMMENDED_SUFFIX", "")
2734
2734
  rerun_suffix = t["options"].get("_RERUN_SUFFIX", "")
2735
2735
  next_suffix = t["options"].get("_NEXT_SUFFIX", "")
2736
+ approve_suffix = t["options"].get("_APPROVE_SUFFIX", recommended_suffix)
2737
+ blocked_rerun_suffix = t["options"].get("_BLOCKED_RERUN_SUFFIX", rerun_suffix)
2736
2738
  description_by_type = dict(TASK_TYPES)
2737
2739
  options: list[Option] = []
2738
2740
 
@@ -2758,10 +2760,21 @@ def _build_task_type(state: WizardState) -> Prompt:
2758
2760
  # `ready` 가 아니면 추천은 비고, 아래 `currentPhase` 재실행 옵션이 남는다.
2759
2761
  # 실패한 run 의 포인터가 `{"phase": "", "status": "blocked"}` 라는 점에서
2760
2762
  # 그것이 맞는 제안이다 — 그 옵션은 포인터가 아니라 `currentPhase` 에서 온다.
2763
+ # 계획 승인 대기는 `awaitingApproval` 로 표시한다. 구현이 추천이지만 먼저
2764
+ # 승인을 받아야 하므로 접미사로 구분한다. 열린 C-NNN 때문에 blocked 면
2765
+ # 재실행은 답이 기록된 뒤에만 고르라고 접미사로 말한다.
2761
2766
  recommended = (revision_requested or state.task_type
2762
2767
  or next_phase.autofill_task_type({"workflow": workflow}))
2763
2768
  if not recommended and not workflow:
2764
2769
  recommended = TASK_TYPE_VALUES[0]
2770
+ pointer = next_phase.promote(workflow.get("nextRecommendedPhase"))
2771
+ if workflow.get("awaitingApproval") is True and recommended == "implementation":
2772
+ recommended_suffix = approve_suffix
2773
+ if (
2774
+ pointer["status"] == next_phase.STATUS_BLOCKED
2775
+ and (workflow.get("currentPhase") or "") == "implementation-planning"
2776
+ ):
2777
+ rerun_suffix = blocked_rerun_suffix
2765
2778
  add(recommended, recommended_suffix)
2766
2779
  add(workflow.get("currentPhase") or "", rerun_suffix)
2767
2780
  add(_phase_after(recommended), next_suffix)
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: okstra-chat
3
+ description: Use when the user wants to create or join a global okstra chat room, send a message to everyone or to one participant, read unread arrivals, or reopen the inbox. Trigger words include "okstra chat", "okstra-chat", "chat room", "join the room", "send a chat message".
4
+ ---
5
+
6
+ # okstra-chat
7
+
8
+ Cross-session rooms in the global okstra home. Not a project task artifact.
9
+ Do not write JSON. Call `okstra chat` and read its fixed text.
10
+
11
+ Rooms are independent of tasks and runs. A participant is this host session.
12
+ The display name is typed at join. Do not invent a default name.
13
+
14
+ ## When to use
15
+
16
+ - The user wants a room that a Claude lead and a Grok lead can both join.
17
+ - The user wants to send a message, see unread arrivals, or reopen the inbox.
18
+
19
+ ## Step 0: CLI
20
+
21
+ Run as a separate Bash tool call with literal leading token:
22
+
23
+ ```bash
24
+ okstra chat --help
25
+ ```
26
+
27
+ If `okstra` is not on PATH, tell the user:
28
+
29
+ `okstra not installed — run npx okstra@latest install once, then retry this skill.`
30
+
31
+ Do not use `npx` from this skill.
32
+
33
+ ## Step 1: Create or join
34
+
35
+ Ask one question: create a room, or join an existing room.
36
+
37
+ If the host has a native picker, use it. Otherwise print a numbered list.
38
+
39
+ ### Create
40
+
41
+ 1. Ask for the room name as free input.
42
+ 2. Run `okstra chat create --room <room>`.
43
+ 3. Ask for the display name as free input. Do not suggest a generated name.
44
+ 4. Run `okstra chat join --room <room> --name <display>`.
45
+
46
+ ### Join
47
+
48
+ 1. Run `okstra chat rooms`.
49
+ 2. If the output is `no rooms`, say so and offer create.
50
+ 3. Otherwise pick a room from that list (picker, or numbered list if the picker limit is exceeded).
51
+ 4. Ask for the display name as free input.
52
+ 5. Run `okstra chat join --room <room> --name <display>`.
53
+
54
+ `--name` is required. An empty name, `all`, or a name already in the room fails.
55
+
56
+ ## Step 2: Unread
57
+
58
+ ```bash
59
+ okstra chat unread --room <room> --as <display>
60
+ ```
61
+
62
+ Show the rows. Each row is `id:time:@from:to:body`. Recipient `all` stays `all`. Sender is always `@name`.
63
+
64
+ If the output is `no unread`, do not ack.
65
+
66
+ If there are unread rows, always run:
67
+
68
+ ```bash
69
+ okstra chat ack --room <room> --as <display> --through <id>
70
+ ```
71
+
72
+ Use the last unread row's id. CLI unread does not move the cursor; this ack does.
73
+
74
+ ## Step 3: Next action
75
+
76
+ Ask: send, unread, inbox, log, or done.
77
+
78
+ There is no ack menu item. Choosing unread again shows the rows then acks, same as Step 2.
79
+
80
+ ### Send
81
+
82
+ 1. Run `okstra chat members --room <room>`.
83
+ 2. Pick the recipient from `all` plus those names except the current display name (`--as`). Filter after `okstra chat members`. Do not pass `--as` to `members`. Recipient is required.
84
+ 3. Ask for the body as free input.
85
+ 4. Run `okstra chat send --room <room> --as <display> --to <all|name> --body <text>`.
86
+
87
+ ### Inbox
88
+
89
+ ```bash
90
+ okstra chat inbox --room <room> --as <display>
91
+ ```
92
+
93
+ This is every arrival to `@you` or `all`, including messages already acked and messages you sent to `all` or to yourself. Inbox does not move the cursor.
94
+
95
+ ### Log
96
+
97
+ ```bash
98
+ okstra chat log --room <room> --as <display>
99
+ ```
100
+
101
+ The whole room, including messages not addressed to you. Log does not move the cursor.
102
+
103
+ ## Rules
104
+
105
+ - Call only `okstra chat`. Do not open files under the chat store.
106
+ - Do not treat chat rows as evidence for a finding, verdict, or assignment.
107
+ - Workers may run the same commands with `--name` and `--as`. Joining is optional.
108
+ - Do not generate a display name from the provider, model, or execution label.
@@ -95,12 +95,13 @@ It has already promoted legacy values.
95
95
  The status response always includes one of:
96
96
 
97
97
  1. **Resume current run** — if `latestResumeCommandPath` exists, display that path.
98
- 2. **Restart current phase** — task can be re-run with the same `task-key` and current `taskType`.
99
- Branches 3–5 are decided by `workflow.nextRecommendedPhase.status` alone — one status, one branch, no other field consulted:
98
+ 2. **Ask the user to approve** — if `workflow.awaitingApproval` is true. Tell the user to approve the plan (`okstra-run` with `--task-type implementation`, which asks `approve_plan_confirm`, or `--approve`). Quote `nextRecommendedPhase.rationale`. A `ready` pointer to `implementation` here means implementation is next after approval, not that it may launch as if already approved. Do not re-run `implementation-planning`.
99
+ 3. **Restart current phase** — only when `awaitingApproval` is false and the pointer is not `ready`. The task can be re-run with the same `task-key` and current `taskType`.
100
+ Branches 4–6 are decided by `workflow.nextRecommendedPhase.status` when `awaitingApproval` is false — one status, one branch:
100
101
 
101
- 3. **Start next phase** — `status` is `ready`. Propose `nextRecommendedPhase.phase` as the next run's `--task-type` and quote its `rationale` as the reason. This is the only status under which a named phase may be launched, so it is the only branch that proposes a run. A `ready` pointer to `release-handoff` already implies an `accepted` final-verification verdict (the report validator refuses that routing target otherwise), so do not re-gate it here.
102
- 4. **Need more information** — `status` is `pending` (the last run did not settle where this task goes next) or `blocked` (it did settle, and the answer is that something outside the run has to change first). Neither proposes a run, and a leftover `phase` name does not change that — `prepare` keeps the name when it lowers a pointer to `pending`, so read `status`, not the emptiness of `phase`. Show the `rationale`, and for `blocked` state what it names as the obstacle. The way forward is a fresh brief, a clarification response, or a re-run of the current phase — not a next-phase launch.
103
- 5. **Task complete (terminal)** — `status` is `terminal`: the task lifecycle ends here. This is **not** a "next phase" — do not propose a new okstra run. Surface the latest report and ask the user whether any follow-up task should be opened separately.
102
+ 4. **Start next phase** — `status` is `ready` and `awaitingApproval` is false. Propose `nextRecommendedPhase.phase` as the next run's `--task-type` and quote its `rationale` as the reason. This is the only status under which a named phase may be launched without a prior approval ask, so it is the only branch that proposes a run. A `ready` pointer to `release-handoff` already implies an `accepted` final-verification verdict (the report validator refuses that routing target otherwise), so do not re-gate it here.
103
+ 5. **Need more information** — `status` is `pending` (the last run did not settle where this task goes next) or `blocked` (it did settle, and the answer is that something outside the run has to change first). Neither proposes a run, and a leftover `phase` name does not change that — `prepare` keeps the name when it lowers a pointer to `pending`, so read `status`, not the emptiness of `phase`. Show the `rationale`, and for `blocked` state what it names as the obstacle. After `implementation-planning`, the first action is `okstra-user-response` on the named `C-NNN` ids; do not re-run planning until those answers exist, and do not start implementation.
104
+ 6. **Task complete (terminal)** — `status` is `terminal`: the task lifecycle ends here. This is **not** a "next phase" — do not propose a new okstra run. Surface the latest report and ask the user whether any follow-up task should be opened separately.
104
105
 
105
106
  ### status.4 — Update workStatus (write)
106
107
 
@@ -241,7 +241,7 @@ If an action has an unknown `command`, `key`, or `scope`, stop and report the wi
241
241
 
242
242
  Before rendering the next phase's bundle — and between worker rounds within a phase (reverify/critic/gapverify batches), after you have collected that round's results and token usage and before you dispatch the next round — close the panes of the dispatches that finished in the prior round so they do not accumulate, in two passes. First count: `okstra team reclaim --project-root <projectRoot> --run-manifest <RUN_MANIFEST_PATH> --dry-run` closes nothing and prints one `<paneId>\t<kind>` line per pane it would close — count those lines as `<n>`. Then run the same command **without** `--dry-run` to close them, and emit `PROGRESS: phase-batch-cleanup panes=<n>` with that count at the batch boundary. The command reads each dispatch's recorded status, so an in-progress worker keeps its pane whichever moment you call it. It closes only the panes okstra opened and recorded — a pane the harness opened for its own teammate carries no recorded id and is not okstra's to close. `shutdown_request` alone only idles the agent and frees no pane, so it stays part of the run-end sequence for roster/token hygiene. A `cli-wrapper` run holds no pane at all, so `<n>` is `0` — still emit the checkpoint.
243
243
 
244
- Before you ask the user for any approval, clarification, or decision after workers have been dispatched, run the same two passes first: `okstra team reclaim … --dry-run` to count the panes, then the same command without `--dry-run` to close them, emit `PROGRESS: phase-gate-cleanup panes=<n>`, and `TaskStop` each completed worker. A `TaskStop` by itself idles the task but leaves the pane open — the `team reclaim` call is what closes it. This keeps a user gate from being shown while finished worker panes remain; in-progress dispatches keep their panes.
244
+ Before you ask the user for any approval, clarification, or decision after workers have been dispatched, run the same two passes first: `okstra team reclaim … --dry-run` to count the panes, then the same command without `--dry-run` to close them, emit `PROGRESS: phase-gate-cleanup panes=<n>`, and `TaskStop` each completed worker. A `TaskStop` by itself idles the task but leaves the pane open — the `team reclaim` call is what closes it. This keeps a user gate from being shown while finished worker panes remain; in-progress dispatches keep their panes. Then follow `prompts/lead/okstra-lead-contract.md` "User confirmation before an approval blocker": read cited plan items, worker findings, and files before asking, and ask in the user's language with each option's outcome.
245
245
 
246
246
  Build the `okstra render-bundle` invocation from `outcome.renderArgv`, passing every token verbatim and in order (including empty strings — they are intentional `use phase default` markers).
247
247
 
@@ -390,4 +390,4 @@ Do not read the wizard state file directly. `okstra wizard outcome` exposes any
390
390
 
391
391
  - Echo each captured answer (`result.echo`) on one short line so the user sees what was registered.
392
392
  - Never invent identity; if a `text` prompt returns an empty answer where the wizard rejects it, the user must retry.
393
- - After Step 6, begin the lead workflow without re-summarizing the skill itself. For a single run, the end of Step 6 is the end of the run — but in an unattended chain where `orchestration.chainStages` has 2+ elements, repeat Step 6 per stage until Step 7's queue is empty (or it stops at a "not ready" / exception gate), then finish.
393
+ - After Step 6, begin the lead workflow without re-summarizing the skill itself. For a single run, the end of Step 6 is the end of the run — but in an unattended chain where `orchestration.chainStages` has 2+ elements, repeat Step 6 per stage until Step 7's queue is empty (or it stops at a "not ready" / exception gate), then finish. After an `implementation-planning` run, read `workflow.awaitingApproval` and the next-phase pointer from the task manifest. If awaiting approval, the next sentence to the user is to approve the plan (`okstra-run` → `implementation`, or `--approve`). Do not start another planning run. If the pointer is `blocked`, name the rationale and send the user to `okstra-user-response`; do not re-run planning until those answers exist.
@@ -1,7 +1,7 @@
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 a report or sidecar. It projects the available tasks and one report as fixed text, asks one question at a time, confirms the user's exact answers, and publishes only the user-owned user-responses sidecar through a typed transaction. NOT for starting a run, inspecting a finished task, or generating a brief.
4
+ Use this to answer an okstra task's open clarification questions in-session without hand-editing a report or sidecar. It projects the available tasks and one report as fixed text, reads cited context before asking, asks one question at a time in the user's language with each option's outcome, confirms the user's exact answers, and publishes only the user-owned user-responses sidecar through a typed transaction. NOT for starting a run, inspecting a finished task, or generating a brief.
5
5
  ---
6
6
 
7
7
  # OKSTRA User Response
@@ -13,26 +13,28 @@ The model-facing commands are fixed text reads and typed transaction writes:
13
13
  | Command | Purpose |
14
14
  |---|---|
15
15
  | `user-response list-view` | Show tasks that still await user input. |
16
- | `user-response show-view` | Show questions, choices, resolved context, and current response state. |
16
+ | `user-response show-view` | Show questions, choices, why asked, linked plan items, cited artifacts, resolved context, and current response state. |
17
17
  | `user-response begin` | Open a sidecar transaction for one report identity. |
18
18
  | `user-response answer` | Add or replace one validated clarification answer. |
19
19
  | `user-response plan-decision` | Record an explicit plan decision in the transaction. |
20
20
  | `user-response legacy-report-authoring` | Record legacy report-authoring permission for report contract 2.0 only. |
21
21
  | `user-response finalize` | Atomically merge and publish the user-owned sidecar. |
22
22
 
23
- Do not use the automation-oriented `list` or `show` commands. Do not open a report record to select fields. The fixed views provide every value this skill may use.
23
+ Do not use the automation-oriented `list` or `show` commands. Do not open a report record to select fields. Question text and `options[]` come only from the fixed views. Cited files listed in `show-view` are read only to explain those options.
24
24
 
25
25
  ## Step 0: Preflight
26
26
 
27
+ Use the registered host ID that the current harness declares for this session. Do not infer it from an executable or `PATH`. Do not substitute `claude-code`.
28
+
27
29
  <!-- BEGIN FRAGMENT: bash-invocation-rule -->
28
30
  Run one Bash tool call, starting with the literal token `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` — a non-literal leading token defeats the `Bash(okstra:*)` permission match):
29
31
  <!-- END FRAGMENT: bash-invocation-rule -->
30
32
 
31
33
  ```bash
32
- okstra preflight --runtime claude-code
34
+ okstra preflight --runtime <host-runtime>
33
35
  ```
34
36
 
35
- On `Okstra preflight: failed`, show `Reason` and `Recovery`, then stop. On `Okstra preflight: ready`, carry the fixed `Project root` and `Project ID` lines as literal values.
37
+ On `Okstra preflight: failed`, show `Reason` and `Recovery`, then stop. On `Okstra preflight: ready`, carry the fixed `Project root`, `Project ID`, `Runtime`, and `Relay contract` lines as literal values.
36
38
 
37
39
  <!-- BEGIN FRAGMENT: preflight-outdated-cli -->
38
40
  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).
@@ -48,6 +50,17 @@ okstra paths --field home
48
50
  Every subsequent `okstra <subcmd>` call self-bootstraps its Python path, so this skill never needs `okstra paths --shell` / `export PYTHONPATH=...`.
49
51
  <!-- END FRAGMENT: python-bootstrap-note -->
50
52
 
53
+ ## Host picker
54
+
55
+ Every choice this skill asks — the task pick, each clarification, an explicit plan decision, and the final record confirmation — uses the same host picker as `okstra-run`.
56
+
57
+ Read the absolute path in the fixed `Relay contract` line. In that file, take the `Wizard interaction relay` JSON. Intersect its `semanticFunctions` with the functions this session can actually call, using each `interactions` kind's `function` field. The live harness does not expose tools named `native_single_select`. Keep `native-single` only when `native_single_select` is in that intersection. Keep `nativeLimits`. If `Relay contract` is `-`, native-single is unavailable.
58
+
59
+ - When `native-single` is available and the option count fits `nativeLimits` (unique labels, within min/max): call `interactions.native-single.function` once with one question and every option as `{label, description}` in original order. Do not print a numbered list in chat while the native tool is available. Claude Code's function is `AskUserQuestion`, Grok's is `ask_user_question`, Codex's is `request_user_input` — copy the relay field; do not substitute one name for another.
60
+ - Otherwise render a 1-based numbered Markdown list and wait for the next message. Do not drop options to force the native tool.
61
+
62
+ Never invent a picker function. Never ask the user to type a number when the native tool is available.
63
+
51
64
  ## Step 1: Select a task from the fixed list view
52
65
 
53
66
  ```bash
@@ -56,7 +69,7 @@ okstra user-response list-view --home <resolved-home> --project <projectId> --li
56
69
 
57
70
  The view gives `Task key`, `Task type`, `Report`, open-item counts, and readability status. If the count is zero, answer `No task has open clarification items.` and stop. Do not continue with an unreadable entry.
58
71
 
59
- Present up to three task choices. The final picker option is `Enter directly`, where the user may provide a report path or task key.
72
+ Present up to three task choices through the host picker. The final picker option is `Enter directly`, where the user may provide a report path or task key.
60
73
 
61
74
  ## Step 2: Read the fixed report view
62
75
 
@@ -64,21 +77,41 @@ Present up to three task choices. The final picker option is `Enter directly`, w
64
77
  okstra user-response show-view --report <reportPath> --project-root <projectRoot>
65
78
  ```
66
79
 
67
- The view contains the report identity, contract version, every open clarification question, its expected form, its current response and disposition, its options, approval context, plan option candidates, current plan decision, and resolved context. It is the only report-information source for this skill.
80
+ The view contains the report identity, contract version, every open clarification question, its expected form, its current response and disposition, its options, approval context, plan option candidates, current plan decision, resolved context, why the row is asked, linked plan items, and cited artifacts. Question text and `options[]` come only from this view. Do not open a report record to select fields.
68
81
 
69
82
  Each entry in `options[]` corresponds to `{role, answer, rationale, scopeImpact, addedWork, directionChange, disposition}`. Put the `recommended` option first and suffix its label with `(Recommended)`. Then put the alternatives in view order and finish with `Enter directly`.
70
83
 
71
- Contract 3.0 options also expose `reach` and `scopeEffects`. Contract 3.0 approval-blocking rows expose `approvalContext`. Present those values exactly as the view prints them.
84
+ Contract 3.0 options also expose `reach` and `scopeEffects`. Contract 3.0 approval-blocking rows expose `approvalContext`.
85
+
86
+ When an axis says `not stated in the report`, repeat that text. Do not infer missing report-owned impact. The skill must **never invent it**.
72
87
 
73
- Each option description uses all three impact axes in this order:
88
+ ## Step 2b: Investigate cited context before asking
74
89
 
75
- > `<rationale>` — Scope: `<scopeImpact>` · Added work: `<addedWork>` · Direction: `<directionChange>`
90
+ Do not present a picker from the raw field dump. For each still-open item, read the investigation list the view printed:
76
91
 
77
- When an axis says `not stated in the report`, repeat that text. Do not infer missing impact. The skill must **never invent it**.
92
+ 1. Every `Cited artifacts:` `path:line` — open that file under the project root from preflight. The line number is the starting point, not a license to skip the surrounding function or section.
93
+ 2. Every `Linked plan items:` definition and every `Context:` definition.
94
+
95
+ Stop at that list. Do not search the rest of the repository for extra files. If a cited path is missing or unreadable, say so in the question; do not guess its contents.
96
+
97
+ Investigation explains. It never adds an option, drops an option, or changes the answer that `--option-number` will record.
78
98
 
79
99
  ## Step 3: Ask one clarification at a time
80
100
 
81
- For each item that still needs an answer, show its position, ID, blocking effect, question, expected form, and resolved context. Use one single-select question per clarification.
101
+ Ask in the user's language. Do not lead with `Kind`, `Blocks`, `Expected form`, or `C-NNN`. The question body is:
102
+
103
+ 1. Why this is being asked (`Why asked`, restated so a non-author of the report can follow it).
104
+ 2. What is already decided (`Context` and linked plan items, in one or two sentences).
105
+ 3. The fork (`Question`, restated as a choice the user can act on).
106
+ 4. What stays blocked if they do not answer (`Blocks=approval` → the plan cannot be approved; `Blocks=next-phase` → the next phase cannot start cleanly).
107
+
108
+ Keep the row id at the end of the question, in parentheses, so the later transaction can name it.
109
+
110
+ Use one single-select question per clarification, through the host picker. Each option description uses this order:
111
+
112
+ > If you pick this: `<addedWork>`. What it reverses: `<directionChange>`. Scope: `<reach or scopeImpact>`. Why it is on the board: `<rationale>`.
113
+
114
+ When investigation quoted a cited file, add one more sentence that names the path. That sentence does not replace a `not stated in the report` axis.
82
115
 
83
116
  Use the displayed values to confirm the user's choice. Do not copy a predefined option's answer, disposition, reach, or scope effects into command arguments. The typed command resolves those report-owned fields from its option number.
84
117
 
@@ -88,15 +121,16 @@ Use the displayed values to confirm the user's choice. Do not copy a predefined
88
121
  | Enters an answer | the user's text verbatim | `answer` |
89
122
  | Asks for the item to be presented again | the user's request verbatim | `reframe` |
90
123
 
91
- Copy `kind` from the view. A `reframe` does not satisfy the gate. If the user asks what an item means, explain only from the view and ask the same item again.
124
+ Copy `kind` from the view. A `reframe` does not satisfy the gate. If the user asks what an item means, explain from the view plus the cited files already read, then ask the same item again.
92
125
 
93
126
  ## Step 4: Confirm the complete response
94
127
 
95
- Echo each clarification ID, kind, disposition, value, and rationale. Include any explicit plan decision or legacy report-authoring decision. Ask:
128
+ Echo each clarification ID, kind, disposition, value, and rationale. Include any explicit plan decision or legacy report-authoring decision. Ask through the host picker, two options:
96
129
 
97
- > Record it as shown above? Reply `confirmed` to publish the sidecar.
130
+ 1. `Record as shown` (Recommended)
131
+ 2. `Change an answer`
98
132
 
99
- Do not start a transaction until the user clearly confirms. If the user changes an item, show the complete response again and reconfirm.
133
+ Do not start a transaction until the user picks `Record as shown`. If they pick `Change an answer`, show the complete response again and reconfirm with the same picker. Do not ask them to type `confirmed`.
100
134
 
101
135
  ## Step 5: Begin the typed transaction
102
136
 
@@ -642,6 +642,35 @@ def write_json(path: Path, payload: dict) -> None:
642
642
  path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
643
643
 
644
644
 
645
+ def _report_already_approved(report_data: Mapping[str, Any] | None) -> bool:
646
+ if not isinstance(report_data, Mapping):
647
+ return False
648
+ frontmatter = report_data.get("frontmatter")
649
+ return isinstance(frontmatter, Mapping) and frontmatter.get("approved") is True
650
+
651
+
652
+ def _derive_awaiting_approval(
653
+ *,
654
+ existing: bool,
655
+ validation_status: str,
656
+ current_phase: str,
657
+ pointer: Mapping[str, str],
658
+ report_data: Mapping[str, Any] | None,
659
+ ) -> bool:
660
+ """planning 이 승인 가능한 plan-ready 를 남기면 올리고, implementation
661
+ 이 그 승인을 소비하면 내린다. 차단 게이트나 열린 Blocks=approval 은
662
+ 포인터가 blocked 라 올리지 않는다."""
663
+ if validation_status == "passed" and current_phase == "implementation":
664
+ return False
665
+ if validation_status == "passed" and current_phase == "implementation-planning":
666
+ return (
667
+ pointer.get("phase") == "implementation"
668
+ and pointer.get("status") == next_phase.STATUS_READY
669
+ and not _report_already_approved(report_data)
670
+ )
671
+ return existing
672
+
673
+
645
674
  def update_workflow_metadata(
646
675
  run_manifest: dict,
647
676
  task_manifest: dict,
@@ -693,7 +722,8 @@ def update_workflow_metadata(
693
722
  next_recommended_phase = next_phase.make(
694
723
  phase=projected["phase"],
695
724
  status=projected["status"],
696
- rationale=(
725
+ rationale=projected["rationale"]
726
+ or (
697
727
  "리포트 라우팅에서 투영됨. 리드가 쓴 값과 근거는 "
698
728
  "nextRecommendedPhaseCorrection.authored 에 있다."
699
729
  ),
@@ -713,16 +743,16 @@ def update_workflow_metadata(
713
743
  status=next_phase.STATUS_BLOCKED, rationale=authored["rationale"]
714
744
  )
715
745
 
716
- awaiting_approval = workflow.get("awaitingApproval")
717
- if not isinstance(awaiting_approval, bool):
718
- awaiting_approval = False
719
- # 승인 게이트(`frontmatter approved`)는 implementation 진입 직전에 한 번만 의미를 가진다.
720
- # implementation run 이 검증을 통과했다는 것은 `_validate_approved_plan` 이 이미 사용자
721
- # 승인 플래그(frontmatter `approved: true`)를 소비했다는 뜻이므로, 이 시점에
722
- # awaitingApproval 플래그를 명시적으로 내려 다음 phase 의 status 뷰에서 stale 상태로
723
- # 남지 않게 한다.
724
- if validation_status == "passed" and current_phase == "implementation":
725
- awaiting_approval = False
746
+ awaiting_existing = workflow.get("awaitingApproval")
747
+ if not isinstance(awaiting_existing, bool):
748
+ awaiting_existing = False
749
+ awaiting_approval = _derive_awaiting_approval(
750
+ existing=awaiting_existing,
751
+ validation_status=validation_status,
752
+ current_phase=current_phase,
753
+ pointer=next_recommended_phase,
754
+ report_data=report_data,
755
+ )
726
756
 
727
757
  last_safe_checkpoint = workflow.get("lastSafeCheckpoint", {})
728
758
  if not isinstance(last_safe_checkpoint, dict):
@@ -3562,6 +3592,7 @@ def validate_final_report_data(
3562
3592
  _validate_clarification_evidence_note(data, failures)
3563
3593
  _validate_approval_clarification_backtrace(data, failures)
3564
3594
  _validate_rerun_guidance(data, failures)
3595
+ _validate_approval_guidance(data, failures)
3565
3596
  _validate_variation_point_analysis(
3566
3597
  (data.get("implementationPlanning") or {}).get("variationPointAnalysis"),
3567
3598
  resolve_architecture(_project_root_from_report(report_path)),
@@ -4147,10 +4178,11 @@ def _stage_scope_bucket(item: dict, pbv: dict) -> str:
4147
4178
 
4148
4179
  Returns `in-scope` (may block), `observed` (only frozen stages), or
4149
4180
  `deferred` (only stages not yet startable). Anything unresolvable is
4150
- `in-scope`: an absent ledger is no basis to narrow, and an item with no
4151
- `stageScope` belongs to the plan as a whole — `P-Opt-*` and `P-Var-*` live
4152
- there permanently, and scoping them out would stop an unrequested-work
4153
- verdict from blocking a start.
4181
+ `in-scope`: an absent ledger is no basis to narrow. Plan-wide items
4182
+ (`P-Opt-*`, `P-Var-*`, `P-Dep-*`, `P-Dir-1`) with no `stageScope` stay
4183
+ in-scope. An unscoped `P-Val-*` / `P-Req-*` / `P-Rb-*` stays in-scope only
4184
+ until a stage is `done`; after that it is `deferred` so a re-plan does not
4185
+ re-score the whole checklist.
4154
4186
 
4155
4187
  디스패치 큐와 같은 함수를 쓴다. 검증기가 다른 통을 내면 워커가 안 본
4156
4188
  항목이 승인을 막거나, 본 항목이 게이트에서 빠진다.
@@ -6130,16 +6162,18 @@ def _validate_approval_clarification_backtrace(
6130
6162
  f"final-report data.json: clarification `{row_id}` blocks approval "
6131
6163
  "and is linked, but the link resolves to no stage. `incremental-"
6132
6164
  "scope` reads the stage from a `P-Step-<stage>.<step>` / `P-Prep-"
6133
- "S<stage>-<kind>` plan-item id, or from a `Stage N` citation in the "
6134
- f"blocked coverage row's `coveredBy`. A `P-Req-*` / `P-Val-*` id "
6135
- "carries no stage number, so a row linked only that way must cite "
6136
- "the stage in `coveredBy`. A blocker whose blast radius resolves to "
6137
- "no stage cannot auto-narrow the next re-run; this report fails "
6138
- "rather than forcing a full re-run."
6165
+ "S<stage>-<kind>` plan-item id, from `stageScope` / `stageRefs` on "
6166
+ "the linked plan item or coverage row, or from a `Stage N` citation "
6167
+ f"in the blocked coverage row's `coveredBy`. A `P-Req-*` / `P-Val-*` "
6168
+ "id carries no stage number, so a row linked only that way must "
6169
+ "carry `stageRefs` or cite the stage in `coveredBy`. A blocker "
6170
+ "whose blast radius resolves to no stage cannot auto-narrow the "
6171
+ "next re-run; this report fails rather than forcing a full re-run."
6139
6172
  )
6140
6173
 
6141
6174
 
6142
6175
  _RERUN_FLAG = "--answered-clarifications"
6176
+ _APPROVE_HINT = re.compile(r"--approve|\bapprov", re.IGNORECASE)
6143
6177
 
6144
6178
 
6145
6179
  def _next_step_texts(steps: object) -> list[str]:
@@ -6156,6 +6190,24 @@ def _next_step_texts(steps: object) -> list[str]:
6156
6190
  return texts
6157
6191
 
6158
6192
 
6193
+ def _has_blocks_approval_row(data: dict) -> bool:
6194
+ return any(
6195
+ isinstance(row, dict) and row.get("blocks") == "approval"
6196
+ for row in data.get("clarificationItems") or []
6197
+ )
6198
+
6199
+
6200
+ def _planning_gate_blocks_approval(data: dict) -> bool:
6201
+ planning = data.get("implementationPlanning")
6202
+ if not isinstance(planning, dict):
6203
+ return False
6204
+ verification = planning.get("planBodyVerification")
6205
+ if not isinstance(verification, dict):
6206
+ return False
6207
+ gate = str(verification.get("gateResult") or "").strip().lower()
6208
+ return gate in {"blocked-by-disagreement", "aborted-non-result"}
6209
+
6210
+
6159
6211
  def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
6160
6212
  """A report that withholds approval must say how to come back from it.
6161
6213
 
@@ -6168,10 +6220,7 @@ def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
6168
6220
  """
6169
6221
  if (data.get("header") or {}).get("taskType") != "implementation-planning":
6170
6222
  return
6171
- has_blocker = any(
6172
- isinstance(row, dict) and row.get("blocks") == "approval"
6173
- for row in data.get("clarificationItems") or []
6174
- )
6223
+ has_blocker = _has_blocks_approval_row(data)
6175
6224
  if not has_blocker:
6176
6225
  return
6177
6226
  if any(_RERUN_FLAG in text for text in _next_step_texts(
@@ -6187,6 +6236,34 @@ def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
6187
6236
  )
6188
6237
 
6189
6238
 
6239
+ def _validate_approval_guidance(data: dict, failures: list[str]) -> None:
6240
+ """승인 가능한 plan-ready 는 사용자에게 승인하라고 말해야 한다.
6241
+
6242
+ 포인터가 implementation/ready 여도 승인은 사용자만 뒤집는다. 다음 단계
6243
+ 안내가 계획 재실행이면 승인 칸을 건너뛰고 같은 단계를 다시 돈다.
6244
+ """
6245
+ if (data.get("header") or {}).get("taskType") != "implementation-planning":
6246
+ return
6247
+ planning = data.get("implementationPlanning")
6248
+ if not isinstance(planning, dict) or planning.get("outcome") != "plan-ready":
6249
+ return
6250
+ if _has_blocks_approval_row(data) or _planning_gate_blocks_approval(data):
6251
+ return
6252
+ if _report_already_approved(data):
6253
+ return
6254
+ if any(_APPROVE_HINT.search(text) for text in _next_step_texts(
6255
+ data.get("recommendedNextSteps")
6256
+ )):
6257
+ return
6258
+ failures.append(
6259
+ "final-report data.json: this plan is ready for the user to approve, "
6260
+ "but no `recommendedNextSteps` entry tells the reader to approve — "
6261
+ "name `--approve` or the in-session wizard in a step's `text` or "
6262
+ "one of its `commands`. Do not recommend another "
6263
+ "implementation-planning run."
6264
+ )
6265
+
6266
+
6190
6267
  def _validate_self_fix_grouping(data: dict, failures: list[str]) -> None:
6191
6268
  """A self-fix round must be instructed by cause, not as a flat item list.
6192
6269
 
@@ -8163,11 +8240,11 @@ def _validate_plan_body_clarification_matching(
8163
8240
  failures: list[str],
8164
8241
  accepted_item_ids: set[str] | None = None,
8165
8242
  ) -> None:
8166
- """H5 — every plan item that the recorded verdicts make `majority-disagree`
8167
- must point (via `clarificationId`) at an existing `blocks: approval`
8168
- clarification row. Closes the hole where a majority-disagree item blocks the
8169
- gate but no §1 Clarification row is raised, so the user never sees why
8170
- approval is withheld (implementation-planning.md self-review step 7).
8243
+ """H5 — every plan item whose *gate class after stage scope* is
8244
+ `majority-disagree` must point at an existing `blocks: approval`
8245
+ clarification row. Observed / deferred / record items stay in `setAside`
8246
+ and must not become a new C row — that is what grew the clarification
8247
+ list while the next stage was already executable.
8171
8248
  """
8172
8249
  ip = data.get("implementationPlanning")
8173
8250
  if not isinstance(ip, dict):
@@ -8189,9 +8266,7 @@ def _validate_plan_body_clarification_matching(
8189
8266
  for item in pbv.get("planItems") or []:
8190
8267
  if not isinstance(item, dict):
8191
8268
  continue
8192
- if _classify_plan_item_gate(item) != "majority-disagree":
8193
- continue
8194
- if _is_dissent_downgraded(item, pbv, accepted):
8269
+ if _plan_item_gate_class(item, pbv, accepted) != "majority-disagree":
8195
8270
  continue
8196
8271
  item_id = item.get("id") or "<unknown>"
8197
8272
  cids = _plan_item_clarification_ids(item)
@@ -8244,7 +8319,7 @@ def _validate_self_fix_before_clarification(data: dict, failures: list[str]) ->
8244
8319
  for item in pbv.get("planItems") or []:
8245
8320
  if not isinstance(item, dict):
8246
8321
  continue
8247
- if _classify_plan_item_gate(item) != "majority-disagree":
8322
+ if _plan_item_gate_class(item, pbv, set()) != "majority-disagree":
8248
8323
  continue
8249
8324
  if _has_planner_fixable_majority(item):
8250
8325
  allowed = " / ".join(sorted(_SELF_FIX_EXHAUSTED_REASONS))