okstra 0.191.1 → 0.191.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.191.1",
3
+ "version": "0.191.2",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.191.1",
3
- "builtAt": "2026-09-08T22:08:39.785Z",
2
+ "package": "0.191.2",
3
+ "builtAt": "2026-09-09T06:51:26.651Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -89,7 +89,7 @@ This section adds report-specific checks to [okstra-lead-contract](./okstra-lead
89
89
  4. When the check reports `mechanical: true` (every entry is a `replace` or `remove`), run `okstra agent-prompt apply-corrections` with the same arguments: okstra writes the corrected narrative to `reportNarrativePath` and records a `lead-correction-applied` activity row naming the ledger and its correction ids. No writer dispatch, `record-dispatch`, or `link-result` follows; the roster row's result already exists.
90
90
  5. Otherwise materialize the writer prompt with the same `--corrections <ledger>` under a new invocation id and prompt path (retire the first attempt's link with `reject-result` as [plan-body-verification](./plan-body-verification.md) describes). okstra renders `## Corrections` (each entry with its label path, current value, replacement or rule, schema constraint, and reason) and `## Output`; the instruction body carries only context.
91
91
 
92
- A report-writer materialization without `--corrections` whose narrative already exists and parses is refused before any prompt is written — free-form corrections cannot be checked before the writer runs, and four of six re-runs in the 2026-09-03 measurement were lead instructions that contradicted the authoring contract. Only a narrative whose structure does not parse (line grammar, an unknown top-level field) is re-authored, not corrected: that dispatch needs no ledger, and its body quotes the parser's message. Value defects — an id outside its pattern, a value outside its enum, a missing required field — leave the structure readable and are exactly what the ledger fixes; the a3 attempt of the 2026-09-03 run carried twenty `SC-` ids that assembly refused and was still a corrective base.
92
+ A report-writer materialization without `--corrections` whose narrative already exists and parses is refused before any prompt is written — free-form corrections cannot be checked before the writer runs, and four of six re-runs in the 2026-09-03 measurement were lead instructions that contradicted the authoring contract. Only a narrative whose structure does not parse (line grammar, an unknown top-level field) is re-authored, not corrected: that dispatch needs no ledger, and its body quotes the parser's message. A narrative that breaks the line grammar is not a produced artifact: the dispatcher settles that attempt as `required worker artifact is unusable: narrative does not parse: …` and retries it inside the same batch, so you see the parser's message at collection, not at Phase 7 assembly (**Enforced:** `okstra_ctl.dispatch_state.unusable_result_defect`, read by `missing_completion_paths` and the `team await` record path). The synthesis packet's Authoring Contract carries the line grammar itself (`report_narrative.NARRATIVE_GRAMMAR_INSTRUCTIONS`), so a writer that reads only the packet still sees it. Value defects — an id outside its pattern, a value outside its enum, a missing required field — leave the structure readable and are exactly what the ledger fixes; the a3 attempt of the 2026-09-03 run carried twenty `SC-` ids that assembly refused and was still a corrective base.
93
93
 
94
94
  **Enforced:** `_with_report_writer_sections` / `_refuse_free_form_correction` in `scripts/okstra_ctl/agent/prompt_cli/materialize.py`, `report_corrections.check_corrections`, `agent/prompt_cli/corrections.run_corrections_apply`; `tests/run/test_agent_prompt_corrections.py` and `tests/contract/test_report_writer_v3_contract.py` keep this procedure in the lead contract.
95
95
 
@@ -133,6 +133,8 @@ For `AskUserQuestion`, map each wizard option to the tool's `{label, description
133
133
 
134
134
  For a `host-text` mapping, render each numbered item as its option label followed by its description verbatim; preserve every item and its order. The next user message is the raw answer. Do not translate numbers, CSV members, labels, or values before `okstra wizard step`. A sequential group wraps each raw reply in one compact JSON object keyed by `questions[].step`; the wizard owns normalization.
135
135
 
136
+ The `confirm` prompt's `label` is the selection summary (one line per resolved input, then the question). Pass it verbatim as the `AskUserQuestion` question text — every line, including `(none)` values — with its three options. Do not replace the summary with a table or a prose digest of your own: a line you drop is a setting the user never saw.
137
+
136
138
 
137
139
  ## Semantic operation mapping
138
140
 
@@ -115,6 +115,10 @@ For `request_user_input`, send one to three questions. Each question carries `id
115
115
  For a `host-text` mapping, render each numbered item as its option label followed by its description verbatim; preserve every item and its order. The next user message is the raw answer: do not translate a number such as `1`, a CSV reply such as `1, 3`, an option label, or an option value before `okstra wizard step`. For `sequential-group`, collect one raw reply per question in order and build one compact JSON object keyed by the corresponding `questions[].step`; the wizard owns all normalization.
116
116
 
117
117
 
118
+ ### Confirm step
119
+
120
+ The `confirm` prompt's `label` is the selection summary (one line per resolved input, then the question). Send it verbatim as that question's text — every line, including `(none)` values — and offer its three options. Do not replace the summary with a table or a prose digest of your own; the user is confirming exactly what the wizard resolved, and a line you drop is a setting the user never saw.
121
+
118
122
  ### Runtime-generated selectable screens
119
123
 
120
124
  `wizard/engine.py` adapts choice screens before returning `next` when the session declares `native_single_select`. `wizard/picker_navigation.py` preserves all original choices while paging long lists, collecting multi-selection through toggle/complete choices, and disambiguating duplicate labels. Oversized or unsupported groups are presented one member at a time. These paths are exercised by `tests/domain/wizard/test_picker_navigation.py` and `test_role_model_selection.py`.
@@ -31,6 +31,8 @@ from .dispatch_state import (
31
31
  load_json_object as _load_json_object,
32
32
  link_agent_dispatch_result as _link_agent_dispatch_result,
33
33
  missing_completion_paths as _missing_completion_paths,
34
+ unusable_result_defect,
35
+ _dispatch_worker_key,
34
36
  _plan_verify_result_aliases,
35
37
  mutate_team_state as _mutate_team_state,
36
38
  require_string as _require_string,
@@ -276,6 +278,9 @@ class WorkerOutcome:
276
278
  timeout: bool
277
279
  terminal_stage: str
278
280
  degraded_from: str
281
+ # 파일은 있는데 읽을 수 없는 산출물의 사유 — `missing_completion_paths` 에
282
+ # 그 경로가 함께 들어가고, 실패 사유는 이 문장을 인용한다.
283
+ artifact_defects: tuple[str, ...] = ()
279
284
 
280
285
 
281
286
  @dataclass(frozen=True)
@@ -1953,9 +1958,15 @@ def _outcome_from_completed(handle: WorkerHandle) -> WorkerOutcome:
1953
1958
  timeout=False,
1954
1959
  terminal_stage="exited",
1955
1960
  degraded_from=handle.degraded_from,
1961
+ artifact_defects=_artifact_defects(handle.job),
1956
1962
  )
1957
1963
 
1958
1964
 
1965
+ def _artifact_defects(job: WorkerJob) -> tuple[str, ...]:
1966
+ defect = unusable_result_defect(job.worker_id, job.result_path)
1967
+ return (defect,) if defect else ()
1968
+
1969
+
1959
1970
  def _correct_teardown_marked_dispatches(plan: DispatchPlan) -> None:
1960
1971
  """Let the wrapper's own exit settle a record teardown wrote off.
1961
1972
 
@@ -3123,6 +3134,7 @@ def _outcome_from_status(record: Mapping[str, Any], status) -> WorkerOutcome:
3123
3134
  timeout=status.timeout,
3124
3135
  terminal_stage=status.stage,
3125
3136
  degraded_from=_string_value(record.get("degradedFrom")),
3137
+ artifact_defects=_record_artifact_defects(record),
3126
3138
  )
3127
3139
 
3128
3140
 
@@ -3190,8 +3202,11 @@ def _record_missing_completion_paths(record: Mapping[str, Any]) -> tuple[Path, .
3190
3202
  result_path = Path(_string_value(record.get("resultPath")))
3191
3203
  worker_result = Path(_string_value(record.get("workerResultPath")))
3192
3204
  missing: list[Path] = []
3205
+ worker_id = _dispatch_worker_key(record)
3193
3206
  for path in _record_completion_paths(record):
3194
3207
  if path.is_file():
3208
+ if path == result_path and unusable_result_defect(worker_id, path):
3209
+ missing.append(path)
3195
3210
  continue
3196
3211
  if path in {result_path, worker_result} and any(alias.is_file() for alias in aliases):
3197
3212
  continue
@@ -3199,6 +3214,13 @@ def _record_missing_completion_paths(record: Mapping[str, Any]) -> tuple[Path, .
3199
3214
  return tuple(missing)
3200
3215
 
3201
3216
 
3217
+ def _record_artifact_defects(record: Mapping[str, Any]) -> tuple[str, ...]:
3218
+ defect = unusable_result_defect(
3219
+ _dispatch_worker_key(record), Path(_string_value(record.get("resultPath")))
3220
+ )
3221
+ return (defect,) if defect else ()
3222
+
3223
+
3202
3224
  def _should_retry(outcome: WorkerOutcome, attempt: int) -> bool:
3203
3225
  """결과가 없으면 재시도한다 — 기준은 종료 코드가 아니라 산출물이다.
3204
3226
 
@@ -3245,6 +3267,8 @@ def _failure_reason(outcome: WorkerOutcome) -> str:
3245
3267
  return "CLI ended cleanly without emitting a result event"
3246
3268
  if outcome.returncode != 0:
3247
3269
  return f"wrapper exited with code {outcome.returncode}"
3270
+ if outcome.artifact_defects:
3271
+ return "required worker artifact is unusable: " + "; ".join(outcome.artifact_defects)
3248
3272
  if outcome.missing_completion_paths:
3249
3273
  missing = ", ".join(str(path) for path in outcome.missing_completion_paths)
3250
3274
  return f"required worker artifact was not produced: {missing}"
@@ -53,6 +53,7 @@ from .execution_manifest import (
53
53
  from .execution_mutation_audit import ExecutionMutationAudit, MutationSnapshot
54
54
  from .final_report_paths import final_report_data_path
55
55
  from .report_inputs import report_narrative_path, uses_report_contract_v3
56
+ from .report_narrative import narrative_structure_defect
56
57
  from .worker_prompt_body import REPORT_WRITER_WORKER_ID
57
58
  from .worker_prompt_contract import (
58
59
  PromptRecord,
@@ -1636,10 +1637,33 @@ def dispatch_mode(jobs: Sequence[WorkerJob]) -> str:
1636
1637
  return BACKEND_MIXED
1637
1638
 
1638
1639
 
1640
+ def unusable_result_defect(worker_id: str, result_path: Path) -> str | None:
1641
+ """산출물이 있어도 소비자가 읽을 수 없으면 없는 것이다 — 지금은 서사 한 종류.
1642
+
1643
+ report-writer 의 서사가 줄 문법을 어기면(frontmatter·헤딩으로 된 보통
1644
+ 보고서) 조립이 Phase 7 에서 거절하고, 그때는 배치의 재시도가 이미 지나
1645
+ 리드가 손으로 재저작을 띄워야 한다 — 실측(2026-09-09, jobs implementation
1646
+ stage-2)에서 리드는 그것을 하지 않고 run 을 닫았다. 수집 시점에 "없는
1647
+ 산출물" 로 세면 `_should_retry` 가 같은 배치 안에서 다시 띄운다.
1648
+ """
1649
+ if worker_id != REPORT_WRITER_WORKER_ID or not result_path.is_file():
1650
+ return None
1651
+ try:
1652
+ text = result_path.read_text(encoding="utf-8")
1653
+ except (OSError, UnicodeDecodeError) as exc:
1654
+ return f"narrative is unreadable: {exc}"
1655
+ defect = narrative_structure_defect(text)
1656
+ if defect is None:
1657
+ return None
1658
+ return f"narrative does not parse: {defect}"
1659
+
1660
+
1639
1661
  def missing_completion_paths(job: WorkerJob) -> tuple[Path, ...]:
1640
1662
  missing: list[Path] = []
1641
1663
  for path in job.completion_paths:
1642
1664
  if path.is_file():
1665
+ if path == job.result_path and unusable_result_defect(job.worker_id, path):
1666
+ missing.append(path)
1643
1667
  continue
1644
1668
  # reports seq 와 workerResults seq 가 갈라지면 워커는 다른 쪽
1645
1669
  # 파일명으로 쓴다. 둘 중 하나가 있으면 산출물은 있는 것이다.
@@ -29,6 +29,39 @@ class NarrativeContractError(ValueError):
29
29
  """서사 입력이 보고서 작성자 소유권이나 Markdown 문법을 위반했다."""
30
30
 
31
31
 
32
+ # 작성자에게 도달해야 하는 줄 문법 — 합성 패킷의 Authoring Contract 가 이것을
33
+ # 그대로 싣는다. 문법이 preamble 템플릿에만 있던 동안 작성자는 read-scope
34
+ # 규칙대로 패킷만 읽고 frontmatter + 헤딩으로 된 보통 보고서를 냈다(실측
35
+ # 2026-09-09, jobs implementation stage-2: `# OKSTRA Report Narrative` 0회,
36
+ # 조립 거부, 최종 리포트 미발행).
37
+ NARRATIVE_GRAMMAR_INSTRUCTIONS: tuple[str, ...] = (
38
+ f"Narrative line grammar: the file starts with the line `{TITLE}` and then "
39
+ "contains only three line shapes — `- **Humanised Field Name**` (one field; "
40
+ "nest a child by indenting two more spaces), `- Item <N>` (one array entry, "
41
+ "numbered 1..N without gaps), and `> value` (one scalar; repeat the line for a "
42
+ "multi-line value; `> _none_` for null, an empty object, or an empty array). "
43
+ "Blank lines are ignored.",
44
+ "Every other line is rejected — YAML frontmatter (`---` blocks), Markdown "
45
+ "headings (`#`, `##`, `###`), pipe tables at column 0, code fences, bare "
46
+ "paragraphs, JSON. Put such text inside a `> ` value instead. Report assembly "
47
+ "refuses the file otherwise and the run publishes no report.",
48
+ )
49
+
50
+
51
+ def narrative_structure_defect(markdown: str) -> str | None:
52
+ """줄 문법 위반 메시지, 없으면 None — 수집 시점의 산출물 검사용.
53
+
54
+ 값 결함(enum 밖 값 등)은 보지 않는다; 그것은 교정 원장이 고친다. 구조가
55
+ 깨진 파일은 원장이 해소될 자료가 없어 재저작 대상이고, 그것을 산출물이
56
+ 있는 것으로 세면 결함이 Phase 7 조립까지 숨어 있다 재저작 없이 run 이 닫힌다.
57
+ """
58
+ try:
59
+ _parse_tree(markdown)
60
+ except NarrativeContractError as exc:
61
+ return str(exc)
62
+ return None
63
+
64
+
32
65
  class _Node:
33
66
  def __init__(self, kind: str, label: str, level: int) -> None:
34
67
  self.kind = kind
@@ -30,6 +30,7 @@ from .final_report_schema import task_block_rules, verdict_token_rule
30
30
  from .report_contract import TASK_TYPE_DATA_PROPERTY
31
31
  from .report_markdown import humanise
32
32
  from .report_narrative import NarrativeContractError, allowed_top_level_fields
33
+ from .report_narrative import NARRATIVE_GRAMMAR_INSTRUCTIONS
33
34
 
34
35
 
35
36
  @dataclass(frozen=True)
@@ -250,6 +251,7 @@ class ReportSynthesisPacket:
250
251
  "format": "report-narrative-v3.0",
251
252
  "sourcePolicy": "read-only-synthesis-packet",
252
253
  "instructions": [
254
+ *NARRATIVE_GRAMMAR_INSTRUCTIONS,
253
255
  "Write the complete human-readable report narrative.",
254
256
  "Preserve settled source values, identities, dissent, and user responses.",
255
257
  "Do not invent a value when a source is missing or contradictory.",
@@ -285,6 +287,7 @@ class ReportSynthesisPacket:
285
287
  f"- Task type: `{self.task_type}`",
286
288
  f"- Result path: `{self.result_path}`",
287
289
  "- Output format: `report-narrative-v3.0`",
290
+ *(f"- {text}" for text in NARRATIVE_GRAMMAR_INSTRUCTIONS),
288
291
  "- Input policy: read this synthesis packet as the dispatched source set",
289
292
  "- Responsibility: write the complete human-readable narrative while preserving settled values",
290
293
  "- Runtime-owned values: session identifiers, token usage, estimated cost, "
@@ -96,10 +96,20 @@ def _worktree_preview_line_impl(state: WizardState) -> str:
96
96
 
97
97
 
98
98
  def _build_confirm(state: WizardState) -> Prompt:
99
+ """확인 질문의 본문이 곧 선택 요약이다.
100
+
101
+ 요약을 `okstra wizard confirmation` 의 별도 텍스트로만 두면 리드가 그것을
102
+ 자기 표로 다시 쓰면서 줄을 빠뜨린다(실측 2026-09-08, jobs implementation:
103
+ 리드가 "추가 지시·관련 작업·추가 응답 문서는 모두 없음" 한 줄로 뭉개고
104
+ directive·base-ref·brief 줄을 뺀 표를 냈다). 네이티브 질문 카드는 질문
105
+ 텍스트를 반드시 그리므로 요약을 질문 텍스트에 싣는다 — 리드가 빼놓을
106
+ 자리가 없다. `okstra wizard confirmation` 은 텍스트 호스트와 재표시용으로
107
+ 같은 블록을 낸다.
108
+ """
99
109
  t = _p(state.workspace_root, "confirm")
100
110
  return Prompt(
101
111
  step=S_CONFIRM, kind="pick",
102
- label=t["label"],
112
+ label=f"{confirmation_block(state)}\n\n{t['label']}",
103
113
  options=[_opt(k, v) for k, v in t["options"].items()],
104
114
  echo_template=t["echo_template"],
105
115
  )
@@ -328,4 +338,6 @@ def confirmation_block(state: WizardState) -> str:
328
338
  lines.append(f" handoff scope : {scope}")
329
339
  if state.task_type == "release-handoff" and state.pr_template_path:
330
340
  lines.append(f" pr-template : {state.pr_template_path} ({state.pr_template_scope or 'once'})")
341
+ if state.fix_cycle:
342
+ lines.append(f" fix-cycle : {state.fix_cycle}")
331
343
  return "\n".join(lines)
@@ -215,15 +215,11 @@ When the selected task type is `project-analysis`, `feature-analysis`, or `chang
215
215
 
216
216
  Do not second-guess the wizard. If the next prompt seems out of place, the bug is in `okstra_ctl.wizard`, not in your interpretation of the user's input.
217
217
 
218
- ## Step 4: Show the confirmation block before the final Proceed
218
+ ## Step 4: The confirm step's question text is the selection summary
219
219
 
220
- When `next.step == "confirm"`, before relaying the picker, fetch the human-readable selection summary:
220
+ When `next.step == "confirm"`, the prompt's `label` already carries the full selection summary (`선택 확인:` followed by one line per resolved input — task-type, task-key, brief, base-ref, worktree, every role slot, directive, related-tasks, clarification, stage, …) and ends with the question. Render that `label` **verbatim, every line, as the question text** of the `confirm` picker (Proceed / Edit / Abort) — the same way you render any other pick step's label. Do not re-author it as a table, do not summarise it into prose, do not drop a line because its value is `(none)`: the user confirms what is on screen, and a line you left out is a setting they never saw (observed: a lead's own table omitted `directive`, `base-ref` and `brief`). Render the picker **as the final output of that turn** — emit nothing after the picker call.
221
221
 
222
- ```bash
223
- okstra wizard confirmation --state-file /var/folders/.../okstra-wizard.AbCd.json
224
- ```
225
-
226
- Output: `{ok: true, text: "Selection summary:\n task-type : ...\n ..."}`. Print `text` to the user, then render the `confirm` picker (Proceed / Edit) **as the final output of that turn** — emit nothing after the picker call.
222
+ `okstra wizard confirmation --state-file <path>` returns the same block as `{ok: true, text: ...}` for a text-only host or when the user asks to see the summary again; it is not a substitute for the label.
227
223
 
228
224
  ## Step 5: Render the task bundle
229
225