okstra 0.134.0 → 0.136.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/docs/architecture/storage-model.md +1 -1
  2. package/docs/cli.md +1 -1
  3. package/docs/for-ai/skills/okstra-pr-gen.md +2 -1
  4. package/docs/for-ai/skills/okstra-user-response.md +8 -3
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/antigravity-worker.md +1 -1
  8. package/runtime/agents/workers/codex-worker.md +1 -1
  9. package/runtime/bin/lib/okstra/interactive.sh +24 -67
  10. package/runtime/bin/okstra-antigravity-exec.sh +22 -10
  11. package/runtime/bin/okstra-claude-exec.sh +20 -9
  12. package/runtime/bin/okstra-codex-exec.sh +22 -10
  13. package/runtime/prompts/lead/convergence.md +1 -1
  14. package/runtime/prompts/lead/team-contract.md +2 -2
  15. package/runtime/prompts/profiles/_common-contract.md +0 -1
  16. package/runtime/prompts/profiles/_implementation-executor.md +20 -1
  17. package/runtime/prompts/profiles/final-verification.md +1 -1
  18. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  19. package/runtime/python/okstra_ctl/backfill.py +2 -1
  20. package/runtime/python/okstra_ctl/context_cost.py +3 -2
  21. package/runtime/python/okstra_ctl/convergence_engine.py +11 -2
  22. package/runtime/python/okstra_ctl/error_log_core.py +3 -1
  23. package/runtime/python/okstra_ctl/handoff.py +2 -1
  24. package/runtime/python/okstra_ctl/implementation_outcome.py +6 -5
  25. package/runtime/python/okstra_ctl/models.py +2 -0
  26. package/runtime/python/okstra_ctl/path_hints.py +3 -1
  27. package/runtime/python/okstra_ctl/paths.py +261 -2
  28. package/runtime/python/okstra_ctl/plan_run_root.py +4 -4
  29. package/runtime/python/okstra_ctl/render_final_report.py +1 -1
  30. package/runtime/python/okstra_ctl/run.py +12 -1
  31. package/runtime/python/okstra_ctl/wizard.py +5 -3
  32. package/runtime/python/okstra_ctl/worker_artifact_paths.py +5 -1
  33. package/runtime/python/okstra_token_usage/collect.py +25 -15
  34. package/runtime/python/okstra_token_usage/pricing.py +4 -1
  35. package/runtime/skills/okstra-pr-gen/SKILL.md +24 -0
  36. package/runtime/skills/okstra-user-response/SKILL.md +65 -17
  37. package/runtime/validators/forbidden_actions.py +1 -1
  38. package/runtime/validators/validate-run.py +20 -22
  39. package/runtime/validators/validate_session_conformance.py +1 -1
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  name: okstra-user-response
3
3
  description: >-
4
- Use this to answer an okstra task's open clarification questions in-session, without hand-editing any file. The tell is a request to respond to an okstra run's clarification items or its approval gate — "answer okstra", "I'll answer the questions", "clarification response", "user response", "approve and move on". This skill lists tasks whose latest report still has open clarification blockers, 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.
4
+ Use this to answer an okstra task's open clarification questions in-session, without hand-editing any file. The tell is a request to respond to an okstra run's clarification items or its approval gate — "answer okstra", "I'll answer the questions", "clarification response", "user response", "approve and move on". This skill lists tasks whose latest report still has open clarification blockers, then walks the open C-ids one at a time each rewritten as a self-contained question with background, three concrete answers (recommendation first) plus "Enter directly" — echoes the collected answers back for an explicit "confirmed" acknowledgement, optionally records approval, and writes the `user-responses/` sidecar via `okstra user-response write`. NOT for starting a run (okstra-run), inspecting a finished task (okstra-inspect), or generating a brief (okstra-brief-gen). The skill never picks an answer — it builds the option board and transcribes what the user decides.
5
5
  ---
6
6
 
7
7
  # OKSTRA User Response
8
8
 
9
9
  Single entry point for answering the clarification questions an okstra run left behind (the open `C-*` rows under the final report's `## 1. Clarification Items`) **in-session**, and recording those answers as a `runs/<type>/user-responses/` sidecar. The next `/okstra-run` auto-attaches this sidecar via `--clarification-response`.
10
10
 
11
- **Core principle — the skill never picks an answer for the user.** 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.
11
+ **Core principle — the skill never picks an answer for the user.** It builds the option board — background, a self-contained question, three concrete answers, `Enter directly` — and the user alone picks from it; every `value` is what the user chose or typed. It never calls `write` until the user has explicitly confirmed.
12
+
13
+ **Second principle — one question at a time.** Never batch two clarification items into one question, and never dump the whole open list at the user. Ask item 1, transcribe the answer, then ask item 2.
12
14
 
13
15
  | Sub-command | What it does |
14
16
  |---|---|
@@ -63,7 +65,7 @@ Returns a JSON array (latest report mtime first); each entry:
63
65
 
64
66
  Carry the chosen entry's `reportPath` and `taskKey` forward.
65
67
 
66
- ## Step 2: Show open rows
68
+ ## Step 2: Fetch the open rows (data only — ask nothing yet)
67
69
 
68
70
  ```bash
69
71
  okstra user-response show --report <reportPath>
@@ -71,33 +73,79 @@ okstra user-response show --report <reportPath>
71
73
 
72
74
  Returns `{reportPath, rows: [{id, kind, blocks, status, statement, recommended, alternatives, contextRefs, resolvedRefs}]}`. `resolvedRefs` is `[{ref, definition}]` — the CLI has already looked up what each internal token (`RB-002`, `FU-001`, `§4.7`, …) means in the report body; `definition` is `null` only when the report text alone could not resolve it (e.g. a `path:line` pointer).
73
75
 
74
- **Do NOT paste the raw `statement` as the question.** A `statement` like `Rewrite RB-002 rollback (see §4.7)` is meaningless to the user on its own that is the whole reason this skill exists. For each open `C-*` row, you MUST present:
76
+ This call is a **data fetch, not a presentation step**. Do not print `rows` at the user, and do not paste a raw `statement` as a question a `statement` like `Rewrite RB-002 rollback (see §4.7)` is meaningless on its own, which is the whole reason this skill exists. Announce only the count and the plan:
77
+
78
+ > `<N>` open items — I'll go through them one at a time.
79
+
80
+ Step 3 then turns exactly one row at a time into a question.
81
+
82
+ ## Step 3: Ask one item at a time (loop)
83
+
84
+ Walk the open rows in report order. **One item per `AskUserQuestion` call** — never batch two items into one call, and never pre-announce the items still to come. Ask, transcribe, move on. Head each item with its position and what it is holding up:
85
+
86
+ > **[2/5] C-014** — blocks: approval gate
87
+
88
+ ### 3a. Background first (plain text, above the picker)
89
+
90
+ `AskUserQuestion` carries one sentence of question, so the background goes in the message text right before the call. Write these three blocks, **3–6 lines total** — enough that someone who never read the report can answer:
91
+
92
+ 1. **Situation** — what the run was doing when it stopped at this item, in the user's own domain terms.
93
+ 2. **What is undecided** — the actual fork, with every internal token expanded inline from `resolvedRefs[].definition`; the user must never need to know what `RB-002` is to answer. Say what is stuck right now in plain words: `blocks: approval` → the approval gate stays shut and `implementation` cannot start; `blocks: next-phase` → the next phase cannot begin.
94
+ 3. **What changes with your answer** — what each direction actually causes downstream.
95
+
96
+ Where the background comes from — in this order:
97
+
98
+ - `resolvedRefs[].definition`, which is already resolved and needs no file read;
99
+ - when a `definition` is `null`, or the three blocks would otherwise be empty, **Read** the report `§`/`path:line` that this item's `contextRefs[]` points at and take it from there;
100
+ - **never invent it.** If the report genuinely does not say why, write `The report states nothing beyond the statement itself.` and go straight to the question. Fabricated background corrupts the answer it collects.
101
+
102
+ Close the background with the raw source on one line, so the mapping back to the report stays visible:
103
+
104
+ > Source: `C-014` — "<raw statement>"
105
+
106
+ ### 3b. The picker — three answers plus `Enter directly`
107
+
108
+ One `AskUserQuestion` (single-select), with exactly four options in this order:
109
+
110
+ 1. **The recommendation** — the answer from `recommended` restated in plain language, label suffixed `(Recommended)`; its description carries the rationale (the part of the `recommended` cell after `—`).
111
+ 2. **Alternative** — `alternatives[0]`.
112
+ 3. **Alternative** — `alternatives[1]`.
113
+ 4. **`Enter directly`** — always last, always present.
114
+
115
+ Slots 2–3 fill from `alternatives[]` first. When `alternatives[]` is short, fill each remaining slot with the first of these that applies:
116
+
117
+ - **a concrete candidate the report itself names** — another option row, an approach §2 rejected, a value already in use elsewhere. Its description must open with `Not an okstra proposal — from <where>` and cite that source.
118
+ - **`Reframe this question`** — the item is held rather than answered, and the next run re-asks it.
119
+
120
+ That last entry is what guarantees the picker always reaches three answers plus `Enter directly`. Never pad a slot with an option no source supports, and never mark anything but `recommended` as recommended.
121
+
122
+ Keep each `label` to the answer itself (1–5 words); the description carries the consequence.
123
+
124
+ ### 3c. Transcribe the decision, then move on
75
125
 
76
- - **A rewritten, self-contained question** (lead with this): rewrite `statement` in plain language with **every internal token expanded inline** from its `resolvedRefs` `definition` — the user must never have to know what `RB-002` is to answer. If a token's `definition` is `null`, resolve it yourself **using Read** on the report `§`/`path:line` before asking; only if it is genuinely unresolvable do you say so plainly.
77
- - **`recommended`** — the answer okstra proposed (+ rationale), also stated in plain language. It is only a recommendation.
78
- - **`alternatives[]`** — list them if present, each in plain language.
79
- - **Raw source** (secondary, for traceability): the raw `id` + `statement` so the mapping back to the report stays visible.
126
+ Record one entry `{id, kind, value, rationale?, disposition}`, `kind` copied from the row `show` returned:
80
127
 
81
- ## Step 3: Per-item collect the user's decision (4 branches)
128
+ | The user picks | `value` | `disposition` |
129
+ |---|---|---|
130
+ | Option 1–3 | that option's full answer text, not its short label | `answer` |
131
+ | `Enter directly` → their own answer | the user's utterance verbatim (rationale into `rationale`) | `answer` |
132
+ | `Reframe this question`, or free text asking for it to be re-asked | what the user wants re-asked, verbatim (empty → the raw statement) | `reframe` |
82
133
 
83
- For each open item, the user picks one of four dispositions. You relay and transcribe; you never choose:
134
+ A reframe is not an answer, so it does not satisfy the approval gate.
84
135
 
85
- 1. **Answer** — the user accepts the recommendation or gives their own answer. `disposition: "answer"`, `value` = the user's utterance verbatim.
86
- 2. **Ask-to-explain (fallback)** — Step 2 already expanded every internal token, so the question should already be self-contained. If the user still asks "what does this mean?" or a `resolvedRefs` `definition` was `null`, open the report `§`/`path:line` that this item's `contextRefs[]` points at **using Read** and explain it in plain language. Only explain — **do not pick the answer for the user**; after explaining, hand the decision back to the user.
87
- 3. **Free-form** — the user gives a narrative answer that matches neither a recommendation nor an alternative. `value` = that narrative verbatim, with the rationale in `rationale` if needed. `disposition: "answer"`.
88
- 4. **Hold + reframe** — if the user asks to "re-frame this question itself", record it as `disposition: "reframe"`. Put what the user wants re-asked into `value` verbatim. A reframe is not an answer, so it does not satisfy the approval gate — the next run re-frames that question.
136
+ If the user replies with a question instead of an answer ("what does this mean?"), record nothing: **Read** the `§`/`path:line` from `contextRefs[]`, explain it in plain language, and re-ask the same item with the same four options. Explain only — **do not resolve it for them**; the decision goes back to the user.
89
137
 
90
- Each answer item's JSON shape: `{id, kind, value, rationale?, disposition}`. `kind` uses the same `kind` that `show` gave for that row.
138
+ Echo one line per finished item (`[2/5] C-014 → answer: 60s`), then ask the next one. Do not summarize the whole set until Step 4.
91
139
 
92
140
  ## Step 4: Echo the full sidecar back → explicit "confirmed" gate
93
141
 
94
- Once every open item 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:
142
+ Once the loop has covered every open item, **before** calling `write`, **echo back** everything collected (each `id`, `disposition`, `value`, `rationale` if present, and the approval decision from Step 5 below) to the user as-is. Then ask for explicit confirmation:
95
143
 
96
144
  > Record it as shown above? (Reply `confirmed` to write the sidecar.)
97
145
 
98
146
  - **Never call `write`** until the user says `confirmed` (or gives clear approval).
99
147
  - If the user changes any item, show the echo-back again with the changed value and re-confirm.
100
- - Every `value` must be the user's utterance verbatim — this gate guarantees that.
148
+ - Every `value` must be what the user picked or typed, never a wording you settled on for them — this gate guarantees that.
101
149
 
102
150
  ## Step 5: (Optional) Record approval
103
151
 
@@ -166,7 +166,7 @@ def scan_forbidden_actions(
166
166
  resolve_team_needles_with_source,
167
167
  )
168
168
 
169
- since, until = resolve_run_window(team_state_path, team_state)
169
+ since, until = resolve_run_window(team_state_path, team_state, relax_start=False)
170
170
  lead_sid = (team_state.get("lead") or {}).get("sessionId") or ""
171
171
  team_needles, _needle_source = resolve_team_needles_with_source(
172
172
  team_state, project_root, since, until, projects_root=claude_projects_dir
@@ -43,6 +43,7 @@ from okstra_ctl.conformance import ( # noqa: E402
43
43
  qa_result_from_dict,
44
44
  validate_conformance_manifest,
45
45
  )
46
+ from okstra_ctl.paths import RunRef # noqa: E402
46
47
  from okstra_ctl.md_table import ( # noqa: E402
47
48
  is_separator_row as _is_markdown_separator,
48
49
  split_pipe_row as _split_pipe_row,
@@ -1143,17 +1144,15 @@ def _scope_manifest_entries(manifest: dict, stage_name: str | None) -> dict:
1143
1144
 
1144
1145
 
1145
1146
  def _task_root_from_run_dir(run_dir: Path) -> Path:
1146
- """run_dir 에서 `runs` 디렉터리를 앵커로 task_root 를 복원한다.
1147
+ """run_dir 에서 task_root 를 복원한다.
1147
1148
 
1148
- 일반 task-type: run_dir = task_root/runs/<task-type> task_root.
1149
- implementation(stage 격리): run_dir = task_root/runs/implementation/stage-<N>
1150
- → 같은 task_root. `runs` 를 위로 탐색하므로 stage-<N> 레벨이 추가돼도
1151
- 안전하다. `runs` 가 조상에 없으면 기존 동작(두 단계 위)로 폴백한다.
1149
+ 레이아웃 해석은 `RunRef.from_run_dir` 소유한다. run 디렉터리가 아닌
1150
+ 입력에는 기존 동작( 단계 위)으로 폴백해 검증기가 죽지 않게 한다.
1152
1151
  """
1153
- for parent in run_dir.parents:
1154
- if parent.name == "runs":
1155
- return parent.parent
1156
- return run_dir.parent.parent
1152
+ try:
1153
+ return RunRef.from_run_dir(run_dir).task_root
1154
+ except ValueError:
1155
+ return run_dir.parent.parent
1157
1156
 
1158
1157
 
1159
1158
  def _read_run_inputs_payload(
@@ -3129,19 +3128,15 @@ _PASSING_VERDICT_TOKENS = frozenset({"accepted", "conditional-accept"})
3129
3128
  def _consumers_rows(report_path: Path) -> list[dict] | None:
3130
3129
  """`runs/implementation-planning/consumers.jsonl` rows for this task.
3131
3130
 
3132
- The final-verification report sits at `runs/final-verification/[stage-N/]
3133
- reports/...`, so walk up to the `runs/` directory rather than guessing a
3134
- fixed depth. ``None`` when the file is absent, so the caller does not turn
3135
- a missing artifact into a false accusation.
3131
+ ``None`` when the report path is unreadable as a run reference or the file
3132
+ is absent, so the caller does not turn a missing artifact into a false
3133
+ accusation.
3136
3134
  """
3137
- runs_dir = None
3138
- for parent in report_path.parents:
3139
- if parent.name == "runs":
3140
- runs_dir = parent
3141
- break
3142
- if runs_dir is None:
3135
+ try:
3136
+ ref = RunRef.from_report_path(report_path)
3137
+ except ValueError:
3143
3138
  return None
3144
- path = runs_dir / "implementation-planning" / "consumers.jsonl"
3139
+ path = ref.sibling("implementation-planning").run_dir / "consumers.jsonl"
3145
3140
  if not path.is_file():
3146
3141
  return None
3147
3142
  rows = []
@@ -3497,8 +3492,11 @@ def _validate_stage_carry_sidecar_exists(
3497
3492
  stage = evidence.get("stageNumber")
3498
3493
  if not isinstance(stage, int):
3499
3494
  return
3500
- # `report_path` is `runs/implementation/reports/final-report-...md`.
3501
- carry_path = report_path.parent.parent / "carry" / f"stage-{stage}.json"
3495
+ # Carry sidecars are stage-SHARED: the next stage's carry-in and
3496
+ # `consumers.backfill_done_from_carry` glob them without knowing the
3497
+ # producing run's layout. `RunRef.carry()` owns that flat-vs-staged rule;
3498
+ # resolving it under the stage run dir forced the lead to write it twice.
3499
+ carry_path = RunRef.from_report_path(report_path).carry(stage)
3502
3500
  if not carry_path.exists():
3503
3501
  failures.append(
3504
3502
  f"implementation run declares stage-{stage} sidecar evidence but "
@@ -194,7 +194,7 @@ def _collect_lead_evidence(
194
194
  )
195
195
  from okstra_token_usage.paths import claude_project_dir
196
196
 
197
- since, until = resolve_run_window(team_state_path, team_state)
197
+ since, until = resolve_run_window(team_state_path, team_state, relax_start=False)
198
198
  lead_sid = (team_state.get("lead") or {}).get("sessionId") or ""
199
199
  team_needles, _needle_source = resolve_team_needles_with_source(
200
200
  team_state, project_root, since, until, projects_root=projects_root