okstra 0.150.0 → 0.151.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/cli.md CHANGED
@@ -720,7 +720,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
720
720
  | `okstra convergence apply-critic-gaps --work-state <path> --results <path>` | Apply one verified coverage-critic batch after the main queue reaches a terminal state |
721
721
  | `okstra convergence finalize --work-state <path> --output <path>` | Materialize the terminal schema v1.3 convergence state |
722
722
  | `okstra convergence validate --state <path> --kind <working\|final>` | Validate replayable working state or a terminal final state |
723
- | `okstra convergence example --kind <groups\|round-results\|critic-results>` | Print one deterministic valid input example as JSON |
723
+ | `okstra convergence example --kind <groups\|round-results\|critic-results>` | Print one deterministic valid input example as JSON. `groups` feeds `seed --groups` and `round-results` feeds `apply-round --results`; `critic-results` is the critic worker's own result document and is **not** the `apply-critic-gaps --results` input — that input is the coverage batch the lead assembles from those candidates plus each analyser's vote (`dispatches[]` / `gaps[]` / `modelExecutionValue`) |
724
724
  | `okstra plan-items extract --data <data.json> --output <items.json>` | Deterministically extract the complete implementation-planning `P-*` queue from report-writer data.json |
725
725
  | `okstra plan-items validate --data <data.json> --items <items.json>` | Require the persisted `P-*` queue to match a fresh deterministic extraction exactly |
726
726
  | `okstra config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | Manage persistent settings such as `pr-template-path` with atomic JSON writes |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.150.0",
3
+ "version": "0.151.0",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -38,6 +38,7 @@
38
38
  "test:js": "node --test tests-js/*.test.mjs",
39
39
  "test:py": "python3 -m pytest tests/",
40
40
  "test:workflow": "bash validators/validate-workflow.sh",
41
- "check": "npm run build && npm run test:js && npm run test:py && npm run test:workflow"
41
+ "test:e2e": "bash tests-e2e/run-all.sh",
42
+ "check": "npm run build && npm run test:js && npm run test:py && npm run test:workflow && npm run test:e2e"
42
43
  }
43
44
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.150.0",
3
- "builtAt": "2026-08-05T03:48:23.472Z",
2
+ "package": "0.151.0",
3
+ "builtAt": "2026-08-05T06:06:04.770Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -19,6 +19,14 @@ tools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "TodoWrite", "WebFetch"
19
19
  - The `**Report Language:**` header in your dispatch prompt is already
20
20
  resolved to `en` or `ko` by the lead. Copy it verbatim into
21
21
  `data.json.meta.reportLanguage`. Never write `auto` here.
22
+ - **That header is not the language you write in — you always write English.**
23
+ It names the language the human HTML renders in, and Phase 7 reads it to
24
+ decide whether to dispatch a translator. The data.json is the SSOT every
25
+ later phase, validator and agent reads, so authoring it in anything but
26
+ English splits the record. **Enforced:** `okstra report-finalize` runs
27
+ `report-translate check-source` as its first Phase 7 step and fails the run
28
+ when the data.json's authored prose is not English. You can run that check
29
+ yourself before returning.
22
30
 
23
31
  ## Authority
24
32
 
@@ -44,7 +44,12 @@ if (SCRIPTS_DIR / "okstra_ctl" / "report_translation.py").is_file():
44
44
  elif HOME_LIB.is_dir() and str(HOME_LIB) not in sys.path:
45
45
  sys.path.insert(0, str(HOME_LIB))
46
46
 
47
- from okstra_ctl.report_translation import extract, overlay # noqa: E402
47
+ from okstra_ctl.report_translation import ( # noqa: E402
48
+ HANGUL_PROSE_LIMIT,
49
+ extract,
50
+ hangul_share,
51
+ overlay,
52
+ )
48
53
  from okstra_ctl.final_report_paths import ( # noqa: E402
49
54
  translation_sidecar_path,
50
55
  translation_source_path,
@@ -132,6 +137,28 @@ def cmd_check(args: argparse.Namespace) -> int:
132
137
  return 0
133
138
 
134
139
 
140
+ def cmd_check_source(args: argparse.Namespace) -> int:
141
+ data_path = Path(args.data).resolve()
142
+ share, length = hangul_share(_load(data_path))
143
+ payload = {
144
+ "ok": share < HANGUL_PROSE_LIMIT,
145
+ "hangulShare": round(share, 4),
146
+ "limit": HANGUL_PROSE_LIMIT,
147
+ "proseChars": length,
148
+ }
149
+ print(json.dumps(payload, ensure_ascii=False))
150
+ if not payload["ok"]:
151
+ sys.stderr.write(
152
+ f"error: {data_path.name} was authored in Korean "
153
+ f"({share:.0%} of its prose, limit {HANGUL_PROSE_LIMIT:.0%}). "
154
+ "The data.json is the English SSOT every later phase reads; the "
155
+ "report language selects the human HTML's language and is served "
156
+ "by the Phase 7 translator, not by authoring the SSOT in it.\n"
157
+ )
158
+ return 1
159
+ return 0
160
+
161
+
135
162
  def _parser() -> argparse.ArgumentParser:
136
163
  parser = argparse.ArgumentParser(
137
164
  prog="okstra-report-translate.py",
@@ -146,6 +173,12 @@ def _parser() -> argparse.ArgumentParser:
146
173
  check_cmd = sub.add_parser("check", help="verify a filled sidecar against its report")
147
174
  check_cmd.add_argument("sidecar", help="path to final-report-<type>-<seq>.i18n.<lang>.json")
148
175
  check_cmd.set_defaults(func=cmd_check)
176
+
177
+ source_cmd = sub.add_parser(
178
+ "check-source", help="verify the data.json itself was authored in English"
179
+ )
180
+ source_cmd.add_argument("data", help="path to final-report-<type>-<seq>.data.json")
181
+ source_cmd.set_defaults(func=cmd_check_source)
149
182
  return parser
150
183
 
151
184
 
@@ -175,7 +175,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
175
175
 
176
176
  1. Lead runs `okstra plan-items extract --data <data.json> --output <state>/plan-items-....json`, places the persisted `items[]` verbatim in every verifier prompt with the compact `subject` and lossless `payload`, then runs `okstra plan-items validate --data <data.json> --items <state>/plan-items-....json`. Dispatch only after that exact-match validation succeeds.
177
177
  2. For each analyser worker in the roster (`claude`, `codex`, and `antigravity` if opted in), lead constructs a reverify prompt using the template in §"Plan-body reverify prompt" below.
178
- 3. Dispatch uses the same wrapper infrastructure as finding convergence. The `--role-slug` is `<role>-plan-verify-r<N>`. Result file path: `runs/<task-type>/worker-results/<role-slug>-plan-verify-r<N>-implementation-planning-<seq>.md`.
178
+ 3. Dispatch uses the same wrapper infrastructure as finding convergence, so the `--role-slug` is the same canonical `<role>-worker` that convergence uses — not a round-specific slug. Result file path: `runs/<task-type>/worker-results/<role>-worker-plan-verify-r<N>-implementation-planning-<seq>.md` (e.g. `codex-worker-plan-verify-r1-implementation-planning-003.md`). The `-worker-` token is load-bearing twice over: §"Plan-body reverify prompt" requires the same anchor headers as convergence, whose `**Audit sidecar path:**` is derived by `okstra_ctl.worker_artifact_paths.audit_sidecar_rel()` inserting `-audit-` after that token — a slug without it makes the header underivable and the helper raises. Record each `planItems[].verdicts[].worker` as the same `<role>-worker` string, because provenance compares it to this filename's prefix. **Enforced:** `tests/contract/test_reverify_dispatch_anchors.py` derives the sidecar from the documented name and re-extracts the prefix the provenance resolver uses.
179
179
  **Verdict provenance (BLOCKING).** Every verdict recorded in `planItems[].verdicts[]` MUST trace back to a dispatch that actually returned a result file at the path above. The whole gate — classification, self-fix eligibility, promotion, `gateBlockedBy` — is computed from these votes, so an unbacked vote lets the round be skipped while the gate still reads `passed`. **Enforced:** `validators/validate-run.py` `_validate_plan_body_verdict_provenance` fails any `verdicts[].worker` with no matching `<worker>-plan-verify-r<N>-<task-type>-<seq>.md` result file. Recording a `verification-error` for a dispatch that produced no result is the correct way to represent a failed worker — inventing an `AGREE` is a contract violation.
180
180
 
181
181
  4. After all dispatches return, lead aggregates verdicts per `P-*` item across workers and classifies each:
@@ -176,7 +176,7 @@ Table Generation Rules:
176
176
 
177
177
  Place this section immediately after the execution status table.
178
178
 
179
- Example (English mode shown the renderer substitutes Korean when `meta.reportLanguage = "ko"`):
179
+ Example (the Markdown renders in English whatever `meta.reportLanguage` says):
180
180
 
181
181
  ```markdown
182
182
  ### Token Usage Summary
@@ -233,7 +233,7 @@ The rows below mirror `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.p
233
233
 
234
234
  `Implementation Design Preparation` is NOT in the substring list: `templates/reports/final-report.template.md` §5.5.10 renders that heading from `implementationPlanning.designPreparation`, so it is enforced by the schema + renderer rather than by a heading scan.
235
235
 
236
- The Korean translation in parentheses is optional but the English keyword is mandatory. The body of each section is written in the Report Language per the writing rules below. For non-`implementation-planning` runs, omit this entire block — these headings are NOT validator-checked for other task-types.
236
+ The English keyword is mandatory and the body of each section is written in English, like everything else you author see "Writing Guidelines" below. For non-`implementation-planning` runs, omit this entire block — these headings are NOT validator-checked for other task-types.
237
237
 
238
238
  The final-report template `templates/reports/final-report.template.md` Section 5.5 already encodes this contract — copy that block verbatim and fill in.
239
239
 
@@ -123,7 +123,21 @@ def _parser() -> argparse.ArgumentParser:
123
123
  subparsers = parser.add_subparsers(dest="operation", required=True)
124
124
 
125
125
  example = subparsers.add_parser(
126
- "example", help="print a deterministic input artifact example"
126
+ "example",
127
+ help="print a deterministic input artifact example",
128
+ description=(
129
+ "Print one deterministic valid example as JSON.\n\n"
130
+ "`groups` feeds `seed --groups` and `round-results` feeds "
131
+ "`apply-round --results`, but `critic-results` is the critic "
132
+ "worker's own result document — NOT the `apply-critic-gaps "
133
+ "--results` input. That input is the coverage batch the lead "
134
+ "assembles from those candidates plus each analyser's vote "
135
+ '({schemaVersion, taskKey, mode: "coverage", provider, '
136
+ "modelExecutionValue, dispatches[], gaps[]}); see "
137
+ 'prompts/lead/convergence.md §"Coverage critic". Feeding this '
138
+ "example straight into `apply-critic-gaps` is rejected, by design."
139
+ ),
140
+ formatter_class=argparse.RawDescriptionHelpFormatter,
127
141
  )
128
142
  example.add_argument(
129
143
  "--kind",
@@ -24,12 +24,17 @@ from .final_report_paths import final_report_data_path, final_report_markdown_pa
24
24
  from .paths import task_dir, task_manifest_file
25
25
 
26
26
 
27
+ STEP_CHECK_SOURCE = "check-source"
27
28
  STEP_TOKEN_USAGE = "token-usage"
28
29
  STEP_RENDER_VIEWS = "render-views"
29
30
  STEP_SPAWN_FOLLOWUPS = "spawn-followups"
30
31
  STEP_VALIDATE_RUN = "validate-run"
31
32
 
32
33
  STEP_ORDER = (
34
+ # First, because everything after it derives from the data.json: rendering
35
+ # a Korean SSOT into English chrome, spawning follow-ups from it, and
36
+ # validating it all succeed on a record the next phase cannot read.
37
+ STEP_CHECK_SOURCE,
33
38
  STEP_TOKEN_USAGE,
34
39
  STEP_RENDER_VIEWS,
35
40
  STEP_SPAWN_FOLLOWUPS,
@@ -205,6 +210,19 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
205
210
  """Assemble the ordered Phase 7 argv list. Order is contractual."""
206
211
  markdown_path = ctx.markdown_path
207
212
  return [
213
+ (
214
+ STEP_CHECK_SOURCE,
215
+ [
216
+ sys.executable,
217
+ str(
218
+ resolve_workspace_script(
219
+ ctx.workspace_root, "okstra-report-translate.py"
220
+ )
221
+ ),
222
+ "check-source",
223
+ str(ctx.data_path),
224
+ ],
225
+ ),
208
226
  (
209
227
  STEP_TOKEN_USAGE,
210
228
  [
@@ -393,6 +393,35 @@ def overlay(
393
393
  return out, OverlayReport(applied, untranslated, tuple(sorted(unresolved)))
394
394
 
395
395
 
396
+ # Above this share of Hangul, the prose was authored in Korean rather than
397
+ # quoting some. Measured across real reports: Korean-authored ones sit at
398
+ # 35-38%, English ones that quote a Korean brief or a worker's Korean phrase
399
+ # reach 8% at most. The gap is wide enough that no threshold inside it is
400
+ # delicate. Only the strings `extract` offers are counted — a verbatim quote of
401
+ # the user's request is structural and never reaches this.
402
+ HANGUL_PROSE_LIMIT = 0.20
403
+
404
+
405
+ def hangul_share(data: Mapping[str, Any]) -> tuple[float, int]:
406
+ """Return the Hangul share of the report's authored prose, and its length."""
407
+ text = "".join(extract(data).values())
408
+ if not text:
409
+ return 0.0, 0
410
+ hangul = sum(1 for char in text if "가" <= char <= "힣")
411
+ return hangul / len(text), len(text)
412
+
413
+
414
+ def prose_is_english(data: Mapping[str, Any]) -> bool:
415
+ """Whether the data.json was authored in English, as the SSOT contract requires.
416
+
417
+ The report language names what the human HTML renders in, not what the
418
+ worker writes. A worker that authors Korean anyway hands every later phase,
419
+ validator and agent a record in a language they do not read.
420
+ """
421
+ share, _ = hangul_share(data)
422
+ return share < HANGUL_PROSE_LIMIT
423
+
424
+
396
425
  def _schema_string_keys(schema: Mapping[str, Any], *, enums: bool) -> set[str]:
397
426
  found: set[str] = set()
398
427
 
@@ -61,6 +61,10 @@ from okstra_ctl.mutation_probe import ( # noqa: E402
61
61
  classify_reason,
62
62
  )
63
63
  from okstra_ctl.self_mock_signals import selfmock_path_key # noqa: E402
64
+ from okstra_ctl.report_translation import ( # noqa: E402
65
+ HANGUL_PROSE_LIMIT,
66
+ hangul_share,
67
+ )
64
68
  from okstra_ctl.stage_citations import cited_stage_numbers # noqa: E402
65
69
  from okstra_ctl.workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE # noqa: E402
66
70
  from okstra_ctl.md_table import ( # noqa: E402
@@ -6316,6 +6320,32 @@ def _validate_improvement_discovery(
6316
6320
  failures.append(f"improvement-discovery: {err}")
6317
6321
 
6318
6322
 
6323
+ def _validate_ssot_is_english(data: dict, failures: list[str]) -> None:
6324
+ """Enforce: the final-report data.json is authored in English.
6325
+
6326
+ `meta.reportLanguage` names the language the human HTML renders in, and
6327
+ Phase 7's translator serves it from a sidecar. The data.json itself is the
6328
+ record every later phase, validator and agent reads, so a worker that
6329
+ authors it in the reader's language instead splits the record — and does so
6330
+ silently, because rendering, follow-up spawning and validation all succeed
6331
+ on it.
6332
+
6333
+ `report-finalize` runs the same check as its first step, before anything
6334
+ derives from the report. This is the second gate, for a report that reached
6335
+ validation by some other path.
6336
+ """
6337
+ if not data:
6338
+ return
6339
+ share, length = hangul_share(data)
6340
+ if length and share >= HANGUL_PROSE_LIMIT:
6341
+ failures.append(
6342
+ f"final-report data.json was authored in Korean ({share:.0%} of its "
6343
+ f"prose, limit {HANGUL_PROSE_LIMIT:.0%}). The data.json is the "
6344
+ "English SSOT; meta.reportLanguage selects the human HTML's "
6345
+ "language and is served by the Phase 7 translator sidecar."
6346
+ )
6347
+
6348
+
6319
6349
  def _validate_fix_cycle(run_manifest: dict, data: dict, failures: list[str]) -> None:
6320
6350
  """Enforce: when the run-manifest carries a fixCycleId, the final-report
6321
6351
  data.json MUST contain a fixCycle block whose ``cycle`` matches it.
@@ -7206,6 +7236,7 @@ def main() -> int:
7206
7236
  ),
7207
7237
  )
7208
7238
  validation_data = report_data if isinstance(report_data, Mapping) else {}
7239
+ _validate_ssot_is_english(validation_data, failures)
7209
7240
  _validate_fix_cycle(run_manifest, validation_data, failures)
7210
7241
  validate_report(
7211
7242
  report_path,