okstra 0.131.0 → 0.132.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.131.0",
3
+ "version": "0.132.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.131.0",
3
- "builtAt": "2026-07-22T04:41:31.388Z",
2
+ "package": "0.132.0",
3
+ "builtAt": "2026-07-22T06:44:24.538Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -308,6 +308,66 @@ def _build_index(headings: list, definitions: list, labels: dict) -> list[str]:
308
308
  return block
309
309
 
310
310
 
311
+ # --- Prose ventilation post-render pass -----------------------------------
312
+ # Split prose paragraphs at sentence boundaries so raw markdown reads one
313
+ # sentence per line; the HTML view turns those newlines into <br>
314
+ # (report_views._markdown_to_html). Prose only — table cells already carry
315
+ # their own <br>, and headings / lists / code / blockquotes must stay intact.
316
+ # A leading-whitespace line is a list continuation, not a prose paragraph:
317
+ # only column-0 non-block lines are treated as prose (allowlist).
318
+ _LIST_LINE_RE = re.compile(r"^\s*(?:[-*]\s|\d+\.\s)")
319
+ _SENT_BOUNDARY_RE = re.compile(r"[.?!][ \t]+(?=\S)")
320
+ # Trailing tokens whose dot is not a sentence end (compared lowercased with
321
+ # the trailing dot stripped).
322
+ _ABBREV = {
323
+ "e.g", "i.e", "etc", "cf", "vs", "al", "no", "fig",
324
+ "dr", "mr", "ms", "inc", "ltd", "jr", "sr",
325
+ }
326
+
327
+
328
+ def _ventilate_segment(seg: str) -> str:
329
+ def replace_boundary(match: re.Match) -> str:
330
+ mark = match.start()
331
+ prev = seg[mark - 1] if mark > 0 else ""
332
+ # A number, a bare-digit, or an ellipsis dot is not a sentence end.
333
+ if not prev or prev.isspace() or prev.isdigit() or prev == ".":
334
+ return match.group(0)
335
+ tail = re.search(r"[A-Za-z.]+$", seg[: mark + 1])
336
+ if tail and tail.group(0).rstrip(".").lower() in _ABBREV:
337
+ return match.group(0)
338
+ return seg[mark] + "\n"
339
+
340
+ return _SENT_BOUNDARY_RE.sub(replace_boundary, seg)
341
+
342
+
343
+ def _split_prose_line(line: str) -> str:
344
+ parts = line.split("`")
345
+ for even in range(0, len(parts), 2): # segments outside backtick code spans
346
+ parts[even] = _ventilate_segment(parts[even])
347
+ return "`".join(parts)
348
+
349
+
350
+ def _ventilate_prose(markdown: str) -> str:
351
+ """Break prose paragraphs into one sentence per line. Idempotent: a line
352
+ already holding a single sentence has no internal boundary to split."""
353
+ lines = markdown.split("\n")
354
+ mask = _code_line_mask(lines)
355
+ for i, line in enumerate(lines):
356
+ if mask[i]: # frontmatter / fenced code
357
+ continue
358
+ if not line or line != line.lstrip(): # blank or indented continuation
359
+ continue
360
+ if (
361
+ _HEADING_RE.match(line)
362
+ or line.startswith("|")
363
+ or line.startswith(">")
364
+ or _LIST_LINE_RE.match(line)
365
+ ):
366
+ continue
367
+ lines[i] = _split_prose_line(line)
368
+ return "\n".join(lines)
369
+
370
+
311
371
  def _inject_index_and_anchors(markdown: str, dictionary: dict | None) -> str:
312
372
  """Append scroll anchors + a top-of-report index to a rendered report.
313
373
  Idempotent: re-running on already-anchored markdown is a no-op for
@@ -500,7 +560,8 @@ def render(
500
560
  try:
501
561
  template = env.get_template(template_path.name)
502
562
  rendered = template.render(**data)
503
- return _inject_index_and_anchors(rendered, dictionary)
563
+ rendered = _inject_index_and_anchors(rendered, dictionary)
564
+ return _ventilate_prose(rendered)
504
565
  except I18nError as exc:
505
566
  raise FinalReportRenderError(
506
567
  f"i18n lookup failed while rendering {template_path.name}: {exc}"
@@ -399,7 +399,9 @@ def _markdown_to_html(
399
399
  para_lines.append(ln)
400
400
  i += 1
401
401
  if para_lines:
402
- out.append("<p>" + _inline(" ".join(para_lines)) + "</p>")
402
+ # The renderer's prose ventilation puts one sentence per source
403
+ # line; join with <br> so those breaks survive into the HTML.
404
+ out.append("<p>" + _inline("<br>".join(para_lines)) + "</p>")
403
405
 
404
406
  return "\n".join(out), headings
405
407
 
@@ -78,7 +78,10 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
78
78
  PromptRecord,
79
79
  validate_initial_prompt_records,
80
80
  )
81
- from okstra_ctl.convergence_engine import validate_final_state # noqa: E402
81
+ from okstra_ctl.convergence_engine import ( # noqa: E402
82
+ grouped_input_digest,
83
+ validate_final_state,
84
+ )
82
85
 
83
86
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
84
87
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
@@ -1609,6 +1612,14 @@ def _validate_conformance(
1609
1612
  f"conformance gate BLOCKING for stage {stage_key}: "
1610
1613
  "requires=[] is declaration/contract trouble and cannot be waived"
1611
1614
  )
1615
+ if entry.get("requires") and entry.get("exemption"):
1616
+ failures.append(
1617
+ f"conformance gate BLOCKING for stage {stage_key}: "
1618
+ f"a stage that declares a gated surface (requires="
1619
+ f"{entry.get('requires')}) cannot be exempted — submit a real "
1620
+ "conformance result or record a user-acknowledged waiver. See "
1621
+ "docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md §7.1."
1622
+ )
1612
1623
  results = _load_conformance_results(qa_dir, scoped)
1613
1624
  for verdict in evaluate_conformance(scoped, results):
1614
1625
  if verdict.status == "ADVISORY":
@@ -5081,6 +5092,292 @@ def _validate_convergence_states(run_dir, failures) -> None:
5081
5092
  )
5082
5093
 
5083
5094
 
5095
+ def _nonempty_string(value) -> bool:
5096
+ """True for a non-blank string, matching the schema's `\\S` pattern."""
5097
+ return isinstance(value, str) and value.strip() != ""
5098
+
5099
+
5100
+ # Round-0 grouping filename: `convergence-groups-<task-type>-<seq>.json`. The
5101
+ # `<task-type>-<seq>` suffix is shared verbatim with the worker-result and
5102
+ # working-state basenames, so capture it as one token to reconstruct both.
5103
+ _CONVERGENCE_GROUPS_BASENAME_RE = re.compile(
5104
+ r"^convergence-groups-(?P<suffix>[a-z][a-z-]*?-\d{3})\.json$"
5105
+ )
5106
+
5107
+
5108
+ def _convergence_group_claims(document) -> list[tuple[str, str, str]] | None:
5109
+ """Extract `(findingId, worker, itemId)` provenance claims from a grouping.
5110
+
5111
+ Returns ``None`` when the document does not match the convergence-groups
5112
+ schema shape closely enough to read its provenance fields safely — the
5113
+ caller refuses to judge such a file. Otherwise returns the deduplicated
5114
+ claims drawn from every group's ``sourceItems[]`` and ``discoveredBy`` map.
5115
+ """
5116
+ if not isinstance(document, dict) or not isinstance(document.get("groups"), list):
5117
+ return None
5118
+ claims: list[tuple[str, str, str]] = []
5119
+ seen: set[tuple[str, str, str]] = set()
5120
+ for group in document["groups"]:
5121
+ if not isinstance(group, dict):
5122
+ return None
5123
+ finding_id = group.get("findingId")
5124
+ source_items = group.get("sourceItems")
5125
+ discovered_by = group.get("discoveredBy")
5126
+ if (
5127
+ not _nonempty_string(finding_id)
5128
+ or not isinstance(source_items, list)
5129
+ or not isinstance(discovered_by, dict)
5130
+ ):
5131
+ return None
5132
+ pairs: list[tuple[object, object]] = []
5133
+ for item in source_items:
5134
+ if not isinstance(item, dict):
5135
+ return None
5136
+ pairs.append((item.get("worker"), item.get("itemId")))
5137
+ for worker, discovery in discovered_by.items():
5138
+ if not isinstance(discovery, dict):
5139
+ return None
5140
+ pairs.append((worker, discovery.get("itemId")))
5141
+ for worker, item_id in pairs:
5142
+ if not _nonempty_string(worker) or not _nonempty_string(item_id):
5143
+ return None
5144
+ claim = (finding_id, worker, item_id)
5145
+ if claim not in seen:
5146
+ seen.add(claim)
5147
+ claims.append(claim)
5148
+ return claims
5149
+
5150
+
5151
+ def _convergence_groups_digest_ok(state_dir, suffix: str, document: dict) -> bool:
5152
+ """False only when a recorded groupsDigest exists and no longer matches.
5153
+
5154
+ ``okstra convergence seed`` pins ``grouped_input_digest`` of the grouping
5155
+ into the working state. When that record is present and disagrees with the
5156
+ on-disk grouping, the file was tampered with or written out of band — not
5157
+ evidence of fabrication — so the caller refuses to judge it. A missing
5158
+ working state or missing digest means judge the file as parsed.
5159
+ """
5160
+ work_path = state_dir / f"convergence-work-{suffix}.json"
5161
+ try:
5162
+ work = json.loads(work_path.read_text(encoding="utf-8"))
5163
+ except (OSError, ValueError):
5164
+ return True
5165
+ recorded = work.get("groupsDigest") if isinstance(work, dict) else None
5166
+ if not _nonempty_string(recorded):
5167
+ return True
5168
+ return grouped_input_digest(document) == recorded
5169
+
5170
+
5171
+ def _read_canonical_worker_result(worker_results_dir, worker: str, suffix: str):
5172
+ """Return the `<worker>-<suffix>.md` text, or ``None`` when it is not a
5173
+ resolvable file directly inside ``worker-results/`` (missing, unreadable,
5174
+ or a worker slug that is not a single path component)."""
5175
+ candidate = worker_results_dir / f"{worker}-{suffix}.md"
5176
+ try:
5177
+ if candidate.resolve().parent != worker_results_dir.resolve():
5178
+ return None
5179
+ return candidate.read_text(encoding="utf-8")
5180
+ except (OSError, ValueError):
5181
+ return None
5182
+
5183
+
5184
+ def _id_occurs_wordbounded(text: str, item_id: str) -> bool:
5185
+ """True when `item_id` occurs as a literal not flanked by another
5186
+ identifier char, so `F-7` matches neither `F-70` nor `xF-7`."""
5187
+ pattern = r"(?<![0-9A-Za-z_-])" + re.escape(item_id) + r"(?![0-9A-Za-z_-])"
5188
+ return re.search(pattern, text) is not None
5189
+
5190
+
5191
+ def _validate_convergence_group_provenance(run_dir, failures) -> None:
5192
+ """Every source item a round-0 grouping cites must exist in the worker file.
5193
+
5194
+ The lead mints its own finding IDs, but each ``sourceItems[].itemId`` and
5195
+ ``discoveredBy.<worker>.itemId`` is copied verbatim from a worker's own
5196
+ result. A grouping that cites ``codex-worker:F-007`` when codex never wrote
5197
+ F-007 is a fabricated provenance link the final report cannot expose. This
5198
+ replays that one claim: the cited item ID must occur, word-boundary
5199
+ anchored, in the worker's canonical result file.
5200
+
5201
+ A validator failure blocks user approval, so unjudgeable input is skipped
5202
+ silently: no groups file, unreadable/malformed JSON, a groups file that
5203
+ fails the schema shape, a missing worker result file (provider-unavailable
5204
+ substitution is legitimate), or a recorded groupsDigest that no longer
5205
+ matches.
5206
+ """
5207
+ from pathlib import Path as _Path
5208
+
5209
+ state_dir = _Path(run_dir) / "state"
5210
+ worker_results_dir = _Path(run_dir) / "worker-results"
5211
+ if not state_dir.is_dir() or not worker_results_dir.is_dir():
5212
+ return
5213
+ for groups_path in sorted(state_dir.glob("convergence-groups-*.json")):
5214
+ match = _CONVERGENCE_GROUPS_BASENAME_RE.match(groups_path.name)
5215
+ if match is None:
5216
+ continue
5217
+ suffix = match.group("suffix")
5218
+ try:
5219
+ document = json.loads(groups_path.read_text(encoding="utf-8"))
5220
+ except (OSError, ValueError):
5221
+ continue
5222
+ claims = _convergence_group_claims(document)
5223
+ if claims is None:
5224
+ continue
5225
+ if not _convergence_groups_digest_ok(state_dir, suffix, document):
5226
+ continue
5227
+ contents: dict[str, str | None] = {}
5228
+ for finding_id, worker, item_id in claims:
5229
+ if worker not in contents:
5230
+ contents[worker] = _read_canonical_worker_result(
5231
+ worker_results_dir, worker, suffix
5232
+ )
5233
+ text = contents[worker]
5234
+ if text is None or _id_occurs_wordbounded(text, item_id):
5235
+ continue
5236
+ failures.append(
5237
+ f"convergence groups `{groups_path.name}` group {finding_id} "
5238
+ f"cites source item `{worker}:{item_id}`, but that ID does not "
5239
+ f"occur in the worker's result file `{worker}-{suffix}.md` — a "
5240
+ "grouping may not invent a provenance link to an item the "
5241
+ "worker never reported."
5242
+ )
5243
+
5244
+
5245
+ # Reverify-prompt basename: `<role-slug>-reverify-r<N>-<task-type>-<seq>.md`.
5246
+ # The role slug ends in `-worker` like the worker-result files, but the round
5247
+ # plan's `dispatches[].worker` may carry either that full slug or the bare
5248
+ # provider token (`claude` vs `claude-worker`), depending on how the roster was
5249
+ # seeded — worker resolution below accepts both. `-reverify-r` never occurs
5250
+ # inside a slug, so the non-greedy slug capture stops at that literal anchor.
5251
+ _REVERIFY_PROMPT_BASENAME_RE = re.compile(
5252
+ r"^(?P<slug>[a-z][a-z0-9-]*?)-reverify-r(?P<round>\d+)"
5253
+ r"-(?P<task_type>[a-z][a-z-]*?)-(?P<seq>\d{3})\.md$"
5254
+ )
5255
+
5256
+ # The reverify prompt's finding identifier is the lead-minted `### F-NNN` /
5257
+ # `### C-NNN` H3 heading inside the `## Findings to verify` block. The older
5258
+ # `### VF-N (origin ...)` / `G-NNN` heading shape this token regex deliberately
5259
+ # does not match, so historical prompts parse zero ids and are skipped.
5260
+ _FINDINGS_TO_VERIFY_HEADING_RE = re.compile(r"^##\s+Findings to verify\s*$")
5261
+ _TWO_HASH_HEADING_RE = re.compile(r"^##\s")
5262
+ _FINDING_HEADING_TOKEN_RE = re.compile(r"^###\s+(F-\d+|C-\d+)")
5263
+
5264
+
5265
+ def _reverify_prompt_finding_ids(content: str) -> set[str] | None:
5266
+ """Distinct `### F-NNN`/`### C-NNN` tokens in the `## Findings to verify`
5267
+ block, or ``None`` when the prompt has no such block.
5268
+
5269
+ The `## Response format` section re-echoes the same headings, so the block
5270
+ is bounded at the next `## ` heading and the tokens are collected into a
5271
+ set — order and re-echoed duplicates are irrelevant because votes are keyed
5272
+ by findingId. An empty set means the block exists but yielded no recognized
5273
+ heading (the historical `VF-N`/`G-NNN` shape); the caller skips both.
5274
+ """
5275
+ lines = content.splitlines()
5276
+ start = None
5277
+ for index, line in enumerate(lines):
5278
+ if _FINDINGS_TO_VERIFY_HEADING_RE.match(line):
5279
+ start = index + 1
5280
+ break
5281
+ if start is None:
5282
+ return None
5283
+ ids: set[str] = set()
5284
+ for line in lines[start:]:
5285
+ if _TWO_HASH_HEADING_RE.match(line):
5286
+ break # next `## ` section (e.g. `## Response format`) ends the block
5287
+ match = _FINDING_HEADING_TOKEN_RE.match(line)
5288
+ if match:
5289
+ ids.add(match.group(1))
5290
+ return ids
5291
+
5292
+
5293
+ def _plan_dispatch_finding_ids(plan, prompt_slug: str) -> set[str] | None:
5294
+ """The `findingIds` set of the single `dispatches[]` row the prompt slug
5295
+ resolves to, or ``None`` when the plan shape is malformed or the slug does
5296
+ not resolve to exactly one row (refuse to judge either way).
5297
+
5298
+ A prompt slug `claude-worker` matches a row whose `worker` is `claude-worker`
5299
+ (exact) or `claude` (bare token, `worker + "-worker" == slug`). The round
5300
+ plan has no JSON schema, so the `dispatches[]` shape is validated defensively.
5301
+ """
5302
+ if not isinstance(plan, dict):
5303
+ return None
5304
+ dispatches = plan.get("dispatches")
5305
+ if not isinstance(dispatches, list):
5306
+ return None
5307
+ matched: list[set[str]] = []
5308
+ for row in dispatches:
5309
+ if not isinstance(row, dict):
5310
+ return None
5311
+ worker = row.get("worker")
5312
+ finding_ids = row.get("findingIds")
5313
+ if not _nonempty_string(worker) or not isinstance(finding_ids, list):
5314
+ return None
5315
+ if any(not _nonempty_string(fid) for fid in finding_ids):
5316
+ return None
5317
+ if worker == prompt_slug or f"{worker}-worker" == prompt_slug:
5318
+ matched.append(set(finding_ids))
5319
+ if len(matched) != 1:
5320
+ return None
5321
+ return matched[0]
5322
+
5323
+
5324
+ def _validate_reverify_prompt_matches_plan(run_dir, failures) -> None:
5325
+ """Each reverify prompt must show exactly the findings its plan row assigned.
5326
+
5327
+ `okstra convergence apply-round` checks *who* voted (`votes ⊆ plan`), but
5328
+ nothing checks that the reverify *prompt* carried the findings the plan
5329
+ assigned. A prompt-generation bug or a tampered prompt that drops or adds a
5330
+ `### F-NNN` heading relative to `dispatches[].findingIds` goes undetected,
5331
+ and a vote can be attributed to a finding the worker was never shown. This
5332
+ replays `plan == prompt`, which with the engine's check yields
5333
+ `votes ⊆ what-was-shown`. It stays independent of the engine's own check.
5334
+
5335
+ A validator failure blocks user approval, so any unjudgeable input is
5336
+ skipped silently: no paired plan file for the prompt's (round, task-type,
5337
+ seq) — which also excludes a superseded plan whose seq/task-type differs;
5338
+ no recognizable `## Findings to verify` block or zero headings (guards the
5339
+ historical `VF-N`/`G-NNN` formats); a worker slug that resolves to other
5340
+ than exactly one `dispatches[]` row; and an unreadable/malformed plan.
5341
+ """
5342
+ from pathlib import Path as _Path
5343
+
5344
+ run_dir = _Path(run_dir)
5345
+ prompts_dir = run_dir / "prompts"
5346
+ state_dir = run_dir / "state"
5347
+ if not prompts_dir.is_dir() or not state_dir.is_dir():
5348
+ return
5349
+ for prompt_path in sorted(prompts_dir.glob("*-reverify-r*.md")):
5350
+ match = _REVERIFY_PROMPT_BASENAME_RE.match(prompt_path.name)
5351
+ if match is None:
5352
+ continue
5353
+ round_n = match.group("round")
5354
+ plan_path = state_dir / (
5355
+ f"convergence-round-{round_n}-plan-"
5356
+ f"{match.group('task_type')}-{match.group('seq')}.json"
5357
+ )
5358
+ if not plan_path.is_file():
5359
+ continue # no paired plan (~96% of artifacts) — never infer from the prompt
5360
+ try:
5361
+ content = prompt_path.read_text(encoding="utf-8")
5362
+ plan = json.loads(plan_path.read_text(encoding="utf-8"))
5363
+ except (OSError, ValueError):
5364
+ continue
5365
+ prompt_ids = _reverify_prompt_finding_ids(content)
5366
+ if not prompt_ids: # None (no block) or empty set (no F-/C- headings)
5367
+ continue
5368
+ plan_ids = _plan_dispatch_finding_ids(plan, match.group("slug"))
5369
+ if plan_ids is None or prompt_ids == plan_ids:
5370
+ continue
5371
+ failures.append(
5372
+ f"reverify prompt `{prompt_path.name}` (round {round_n}, worker "
5373
+ f"`{match.group('slug')}`) does not show the findings its plan row "
5374
+ f"assigned: in the plan but not the prompt {sorted(plan_ids - prompt_ids)}; "
5375
+ f"in the prompt but not the plan {sorted(prompt_ids - plan_ids)}. The "
5376
+ f"prompt must carry exactly `dispatches[].findingIds` so a vote cannot "
5377
+ f"be attributed to a finding the worker was never shown."
5378
+ )
5379
+
5380
+
5084
5381
  def _validate_requirements_discovery_fanout(run_dir, failures, brief_path=None) -> None:
5085
5382
  """requirements-discovery run 에 fan-out/ 이 있으면 packet+index 를 검증해
5086
5383
  실패를 ``requirements-discovery: `` 접두로 folding 한다. fan-out 이 없으면 no-op.
@@ -5558,6 +5855,8 @@ def main() -> int:
5558
5855
  _validate_convergence_rounds_match_manifest(
5559
5856
  report_path.parent.parent, run_manifest, failures
5560
5857
  )
5858
+ _validate_convergence_group_provenance(report_path.parent.parent, failures)
5859
+ _validate_reverify_prompt_matches_plan(report_path.parent.parent, failures)
5561
5860
  validate_report_views(report_path, failures)
5562
5861
  if task_type == "implementation":
5563
5862
  _validate_missing_qa_categories_recorded(