okstra 0.158.1 → 0.160.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 (84) hide show
  1. package/README.md +1 -1
  2. package/docs/architecture/storage-model.md +2 -0
  3. package/docs/architecture.md +1 -1
  4. package/docs/cli.md +8 -3
  5. package/docs/for-ai/README.md +2 -2
  6. package/docs/for-ai/skills/okstra-inspect.md +3 -0
  7. package/docs/for-ai/skills/okstra-run.md +2 -1
  8. package/docs/for-ai/skills/okstra-user-response.md +5 -5
  9. package/docs/project-structure-overview.md +5 -1
  10. package/docs/task-process/implementation.md +28 -0
  11. package/package.json +1 -1
  12. package/runtime/BUILD.json +2 -2
  13. package/runtime/agents/workers/report-writer-worker.md +1 -1
  14. package/runtime/bin/okstra-claude-exec.sh +4 -1
  15. package/runtime/prompts/host-orchestration/README.md +18 -0
  16. package/runtime/prompts/host-orchestration/implementation.md +57 -0
  17. package/runtime/prompts/launch.template.md +10 -1
  18. package/runtime/prompts/lead/adapters/claude-code.md +1 -1
  19. package/runtime/prompts/lead/context-loader.md +5 -2
  20. package/runtime/prompts/lead/convergence.md +3 -1
  21. package/runtime/prompts/lead/plan-body-verification.md +21 -2
  22. package/runtime/prompts/lead/report-writer.md +1 -1
  23. package/runtime/prompts/lead/team-contract.md +2 -1
  24. package/runtime/prompts/profiles/_clarification-recommendation.md +11 -1
  25. package/runtime/prompts/profiles/_common-contract.md +3 -1
  26. package/runtime/prompts/profiles/implementation-planning.md +2 -0
  27. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  28. package/runtime/prompts/wizard/prompts.ko.json +3 -0
  29. package/runtime/python/okstra_ctl/clarification_items.py +9 -0
  30. package/runtime/python/okstra_ctl/codex_dispatch.py +6 -6
  31. package/runtime/python/okstra_ctl/convergence.py +168 -11
  32. package/runtime/python/okstra_ctl/dispatch_core.py +4 -2
  33. package/runtime/python/okstra_ctl/error_issue.py +640 -0
  34. package/runtime/python/okstra_ctl/error_report.py +56 -0
  35. package/runtime/python/okstra_ctl/error_zip.py +23 -10
  36. package/runtime/python/okstra_ctl/incremental_scope.py +159 -19
  37. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +18 -5
  38. package/runtime/python/okstra_ctl/issue_signals.py +186 -0
  39. package/runtime/python/okstra_ctl/paths.py +38 -0
  40. package/runtime/python/okstra_ctl/plan_items_cli.py +167 -3
  41. package/runtime/python/okstra_ctl/profile_show.py +134 -0
  42. package/runtime/python/okstra_ctl/recap.py +63 -0
  43. package/runtime/python/okstra_ctl/render_final_report.py +11 -62
  44. package/runtime/python/okstra_ctl/report_html/filters.py +6 -1
  45. package/runtime/python/okstra_ctl/report_html/render.py +9 -8
  46. package/runtime/python/okstra_ctl/report_html/run_usage.py +110 -0
  47. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +69 -16
  48. package/runtime/python/okstra_ctl/report_html/visualizations.py +107 -14
  49. package/runtime/python/okstra_ctl/report_translation.py +4 -0
  50. package/runtime/python/okstra_ctl/report_views.py +7 -3
  51. package/runtime/python/okstra_ctl/run.py +41 -2
  52. package/runtime/python/okstra_ctl/run_audit.py +477 -0
  53. package/runtime/python/okstra_ctl/usage_cells.py +47 -0
  54. package/runtime/python/okstra_ctl/user_response.py +25 -10
  55. package/runtime/python/okstra_ctl/verdict_blocks.py +183 -0
  56. package/runtime/python/okstra_ctl/wizard.py +64 -10
  57. package/runtime/python/okstra_ctl/worker_audit_check.py +44 -0
  58. package/runtime/python/okstra_ctl/worker_audit_ledger.py +207 -0
  59. package/runtime/python/okstra_ctl/worker_heartbeat.py +9 -3
  60. package/runtime/python/okstra_ctl/worker_liveness.py +81 -9
  61. package/runtime/schemas/final-report-v1.0.schema.json +14 -0
  62. package/runtime/schemas/final-report-v2.0.schema.json +56 -2
  63. package/runtime/skills/okstra-inspect/SKILL.md +3 -1
  64. package/runtime/skills/okstra-inspect/facets/error-issue.md +77 -0
  65. package/runtime/skills/okstra-inspect/facets/run-audit.md +34 -0
  66. package/runtime/skills/okstra-run/SKILL.md +28 -10
  67. package/runtime/skills/okstra-user-response/SKILL.md +18 -18
  68. package/runtime/templates/reports/final-report.template.md +4 -0
  69. package/runtime/templates/reports/html/assets/base.css +14 -1
  70. package/runtime/templates/reports/html/base.template.html +42 -0
  71. package/runtime/templates/reports/html/i18n/en.json +30 -1
  72. package/runtime/templates/reports/html/i18n/ko.json +30 -1
  73. package/runtime/templates/reports/html/macros/forms.html +15 -0
  74. package/runtime/templates/reports/html/macros/visualizations.html +3 -2
  75. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -0
  76. package/runtime/templates/reports/i18n/en.json +2 -0
  77. package/runtime/validators/validate-run.py +331 -208
  78. package/runtime/validators/validate_session_conformance.py +102 -32
  79. package/src/cli-registry.mjs +34 -0
  80. package/src/commands/execute/incremental-scope.mjs +10 -0
  81. package/src/commands/execute/worker-audit-check.mjs +35 -0
  82. package/src/commands/inspect/error-issue.mjs +27 -0
  83. package/src/commands/inspect/profile-show.mjs +29 -0
  84. package/src/commands/inspect/run-audit.mjs +26 -0
@@ -54,6 +54,14 @@ DEFAULT_LAUNCH_GRACE_SECONDS = 60
54
54
  DEFAULT_POLL_INTERVAL_SECONDS = 20.0
55
55
  DEFAULT_WAIT_TIMEOUT_SECONDS = 2400.0
56
56
 
57
+ # A budget breach is one observation, and "slow" and "dead" are only
58
+ # distinguishable across two. On a breach the probe re-reads the sidecar this
59
+ # far into the future — a fraction of the stage's own budget, so a stage with a
60
+ # longer budget also gets a longer confirmation. Measured false positives this
61
+ # absorbs (dev-10400): `analysis` 386s against a 360s budget,
62
+ # `data-json-write-start` 1602s against 1260s.
63
+ DEFAULT_STALL_CONFIRM_RATIO = 0.5
64
+
57
65
 
58
66
  def _utc_now() -> datetime:
59
67
  return datetime.now(timezone.utc)
@@ -183,13 +191,58 @@ class ProbeTarget:
183
191
  result_path: Path | None = None
184
192
 
185
193
 
186
- def probe_one(target: ProbeTarget, *, now: datetime, max_idle: float,
187
- launch_grace: float) -> dict:
188
- if target.liveness_mode == LIVENESS_AUDIT_HEARTBEAT:
189
- return probe_heartbeat(
190
- target.artifact, target.dispatched_at, now, max_idle, launch_grace
191
- )
192
- return probe_launch(target.artifact, target.dispatched_at, now, launch_grace)
194
+ def _confirm_window(probe: dict, stall_confirm: float | None) -> float:
195
+ """Seconds to wait before a budget breach becomes a verdict.
196
+
197
+ Only a breach carries ``budgetSeconds``. The other stalled shapes — a
198
+ sidecar with no heartbeat at all, a newest beat that predates this dispatch
199
+ — are not "the worker is mid-tool-call", so waiting tells us nothing new
200
+ about them.
201
+ """
202
+ if "budgetSeconds" not in probe:
203
+ return 0.0
204
+ if stall_confirm is not None:
205
+ return max(0.0, float(stall_confirm))
206
+ return probe["budgetSeconds"] * DEFAULT_STALL_CONFIRM_RATIO
207
+
208
+
209
+ def probe_one(
210
+ target: ProbeTarget,
211
+ *,
212
+ now: datetime,
213
+ max_idle: float,
214
+ launch_grace: float,
215
+ stall_confirm: float | None = None,
216
+ sleep: Callable[[float], None] = time.sleep,
217
+ clock: Callable[[], datetime] = _utc_now,
218
+ ) -> dict:
219
+ """One worker's verdict, with a budget breach confirmed before it stands.
220
+
221
+ The wait this costs is bounded by the confirmation window; what the probe
222
+ exists to avoid is paying ``DEFAULT_WAIT_TIMEOUT_SECONDS`` for a worker that
223
+ died early, and that is still never paid.
224
+ """
225
+ if target.liveness_mode != LIVENESS_AUDIT_HEARTBEAT:
226
+ return probe_launch(target.artifact, target.dispatched_at, now, launch_grace)
227
+ probe = probe_heartbeat(
228
+ target.artifact, target.dispatched_at, now, max_idle, launch_grace
229
+ )
230
+ if probe["state"] != "stalled":
231
+ return probe
232
+ window = _confirm_window(probe, stall_confirm)
233
+ if window <= 0:
234
+ return probe
235
+ sleep(window)
236
+ confirmed = probe_heartbeat(
237
+ target.artifact, target.dispatched_at, clock(), max_idle, launch_grace
238
+ )
239
+ if confirmed.get("lastHeartbeat") == probe.get("lastHeartbeat"):
240
+ return confirmed
241
+ # The question the window asks is whether the heartbeat moved, not whether
242
+ # the re-read is healthy on its own terms. A beat opening a stage with a
243
+ # smaller budget than the window we just slept reads as stale the instant it
244
+ # lands, which would call a worker dead for proving it is alive.
245
+ return {**confirmed, "state": "live", "reason": ""}
193
246
 
194
247
 
195
248
  def probe_all(
@@ -198,9 +251,15 @@ def probe_all(
198
251
  now: datetime,
199
252
  max_idle: float,
200
253
  launch_grace: float,
254
+ stall_confirm: float | None = None,
255
+ sleep: Callable[[float], None] = time.sleep,
256
+ clock: Callable[[], datetime] = _utc_now,
201
257
  ) -> dict:
202
258
  probes = [
203
- probe_one(t, now=now, max_idle=max_idle, launch_grace=launch_grace)
259
+ probe_one(
260
+ t, now=now, max_idle=max_idle, launch_grace=launch_grace,
261
+ stall_confirm=stall_confirm, sleep=sleep, clock=clock,
262
+ )
204
263
  for t in targets
205
264
  ]
206
265
  unhealthy = [p for p in probes if p["state"] in ("stalled", "did-not-launch")]
@@ -221,6 +280,7 @@ def wait_for_results(
221
280
  launch_grace: float,
222
281
  interval: float,
223
282
  timeout: float,
283
+ stall_confirm: float | None = None,
224
284
  clock: Callable[[], datetime] = _utc_now,
225
285
  sleep: Callable[[float], None] = time.sleep,
226
286
  ) -> dict:
@@ -239,7 +299,8 @@ def wait_for_results(
239
299
  while True:
240
300
  now = clock()
241
301
  result = probe_all(
242
- targets, now=now, max_idle=max_idle, launch_grace=launch_grace
302
+ targets, now=now, max_idle=max_idle, launch_grace=launch_grace,
303
+ stall_confirm=stall_confirm, sleep=sleep, clock=clock,
243
304
  )
244
305
  pending = [
245
306
  str(t.result_path) for t in targets if not result_ready(t)
@@ -367,6 +428,15 @@ def main(argv: list[str] | None = None) -> int:
367
428
  help="--wait poll interval in seconds")
368
429
  parser.add_argument("--timeout", type=float, default=DEFAULT_WAIT_TIMEOUT_SECONDS,
369
430
  help="--wait deadline in seconds")
431
+ parser.add_argument(
432
+ "--stall-confirm", type=float, default=None,
433
+ help=(
434
+ "seconds to re-check a heartbeat budget breach before calling it "
435
+ "stalled (default: half that stage's budget; 0 disables). A slow "
436
+ "worker appends its next heartbeat inside this window; a dead one "
437
+ "does not."
438
+ ),
439
+ )
370
440
  args = parser.parse_args(argv)
371
441
 
372
442
  if len(args.team_state) != len(args.worker):
@@ -388,6 +458,7 @@ def main(argv: list[str] | None = None) -> int:
388
458
  now=_utc_now(),
389
459
  max_idle=args.max_idle,
390
460
  launch_grace=args.launch_grace,
461
+ stall_confirm=args.stall_confirm,
391
462
  )
392
463
  print(json.dumps(result, ensure_ascii=False, indent=2))
393
464
  # Non-zero on an unhealthy worker so a poll loop can branch on the exit
@@ -410,6 +481,7 @@ def main(argv: list[str] | None = None) -> int:
410
481
  launch_grace=args.launch_grace,
411
482
  interval=args.interval,
412
483
  timeout=args.timeout,
484
+ stall_confirm=args.stall_confirm,
413
485
  )
414
486
  print(json.dumps(result, ensure_ascii=False, indent=2))
415
487
  return {"completed": 0, "unhealthy": 1}.get(result["outcome"], 2)
@@ -2959,6 +2959,20 @@
2959
2959
  "required": ["roundCount", "gateResult", "planItems", "dissentLog"],
2960
2960
  "additionalProperties": false,
2961
2961
  "properties": {
2962
+ "uniformVerifiers": {
2963
+ "type": "array",
2964
+ "description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
2965
+ "items": {
2966
+ "type": "object",
2967
+ "additionalProperties": false,
2968
+ "required": ["worker", "verdict", "itemCount"],
2969
+ "properties": {
2970
+ "worker": { "type": "string", "minLength": 1 },
2971
+ "verdict": { "type": "string", "minLength": 1 },
2972
+ "itemCount": { "type": "integer", "minimum": 1 }
2973
+ }
2974
+ }
2975
+ },
2962
2976
  "roundCount": { "type": "integer", "minimum": 0 },
2963
2977
  "gateResult": {
2964
2978
  "enum": [
@@ -1637,7 +1637,11 @@
1637
1637
  "items": { "type": "string" }
1638
1638
  },
1639
1639
  "confidence": { "enum": ["low", "medium", "high"] },
1640
- "disproveWith": { "type": "string", "minLength": 1 }
1640
+ "disproveWith": { "type": "string", "minLength": 1 },
1641
+ "downstreamOf": {
1642
+ "type": "array",
1643
+ "items": { "type": "string", "pattern": "^EA-\\d{3,}$" }
1644
+ }
1641
1645
  }
1642
1646
  }
1643
1647
  },
@@ -1699,6 +1703,28 @@
1699
1703
  "enum": ["open", "answered", "resolved", "obsolete"]
1700
1704
  },
1701
1705
 
1706
+ "ClarificationScopeToken": {
1707
+ "enum": ["in-repo", "cross-repo", "new-schema", "deferrable"]
1708
+ },
1709
+
1710
+ "ClarificationOption": {
1711
+ "type": "object",
1712
+ "required": ["role", "answer", "rationale", "scopeImpact", "addedWork", "directionChange"],
1713
+ "additionalProperties": false,
1714
+ "properties": {
1715
+ "role": { "enum": ["recommended", "alternative"] },
1716
+ "answer": { "type": "string", "minLength": 1 },
1717
+ "rationale": { "type": "string", "minLength": 1 },
1718
+ "scopeImpact": {
1719
+ "type": "array",
1720
+ "minItems": 1,
1721
+ "items": { "$ref": "#/$defs/ClarificationScopeToken" }
1722
+ },
1723
+ "addedWork": { "type": "string", "minLength": 1 },
1724
+ "directionChange": { "type": "string", "minLength": 1 }
1725
+ }
1726
+ },
1727
+
1702
1728
  "WorkerStatus": {
1703
1729
  "enum": ["completed", "error", "timeout", "not-run", "synthesis-only"]
1704
1730
  },
@@ -3736,6 +3762,20 @@
3736
3762
  "required": ["roundCount", "gateResult", "planItems", "dissentLog"],
3737
3763
  "additionalProperties": false,
3738
3764
  "properties": {
3765
+ "uniformVerifiers": {
3766
+ "type": "array",
3767
+ "description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
3768
+ "items": {
3769
+ "type": "object",
3770
+ "additionalProperties": false,
3771
+ "required": ["worker", "verdict", "itemCount"],
3772
+ "properties": {
3773
+ "worker": { "type": "string", "minLength": 1 },
3774
+ "verdict": { "type": "string", "minLength": 1 },
3775
+ "itemCount": { "type": "integer", "minimum": 1 }
3776
+ }
3777
+ }
3778
+ },
3739
3779
  "roundCount": { "type": "integer", "minimum": 0 },
3740
3780
  "gateResult": {
3741
3781
  "enum": [
@@ -4074,6 +4114,15 @@
4074
4114
  "type": "object",
4075
4115
  "required": ["id", "ticketId", "kind", "statement", "expectedForm", "blocks", "status"],
4076
4116
  "additionalProperties": false,
4117
+ "allOf": [
4118
+ {
4119
+ "if": {
4120
+ "properties": { "kind": { "const": "decision" } },
4121
+ "required": ["kind"]
4122
+ },
4123
+ "then": { "required": ["options"] }
4124
+ }
4125
+ ],
4077
4126
  "properties": {
4078
4127
  "id": { "type": "string", "pattern": "^C-\\d{3,}$" },
4079
4128
  "ticketId": { "$ref": "#/$defs/TicketId" },
@@ -4082,7 +4131,12 @@
4082
4131
  "expectedForm": { "type": "string", "minLength": 1 },
4083
4132
  "blocks": { "$ref": "#/$defs/ClarificationBlocks" },
4084
4133
  "status": { "$ref": "#/$defs/ClarificationStatus" },
4085
- "userInput": { "type": "string" }
4134
+ "userInput": { "type": "string" },
4135
+ "options": {
4136
+ "type": "array",
4137
+ "minItems": 2,
4138
+ "items": { "$ref": "#/$defs/ClarificationOption" }
4139
+ }
4086
4140
  }
4087
4141
  },
4088
4142
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: okstra-inspect
3
3
  description: >-
4
- Use this for everything that happens AFTER a single okstra task has already run — inspecting it or light bookkeeping on it, never launching new work. The tell is usually a named task id (PROD-1623, dev-9184), often dropped without the word "okstra." Reach for it when the user wants one task's: status, current/next phase, blockers, or approval gate; its final report — where it is or whether it passed (verdict / pass); its elapsed time or context/read cost; its run history, re-run, or resume; to mark it done / in-progress / blocked / todo; or a failed run's error logs gathered into a report (error report). Also bundles cross-project okstra errors into an anonymized feedback zip (error feedback). Use it even for a bare "mark it done" or "where's the report." NOT for starting a run (okstra-run), rollups/schedules (okstra-rollup / okstra-schedule-gen), a brief (okstra-brief-gen), setup (okstra-setup), or cross-project management (okstra-manager).
4
+ Use this for everything that happens AFTER a single okstra task has already run — inspecting it or light bookkeeping on it, never launching new work. The tell is usually a named task id (PROD-1623, dev-9184), often dropped without the word "okstra." Reach for it when the user wants one task's: status, current/next phase, blockers, or approval gate; its final report — where it is or whether it passed (verdict / pass); its elapsed time or context/read cost; its run history, re-run, or resume; to mark it done / in-progress / blocked / todo; or a failed run's error logs gathered into a report (error report). Also turns cross-project okstra errors into an anonymized zip (error feedback) or GitHub issues (file okstra issues), and audits run health across tasks (run audit). NOT for starting a run (okstra-run), rollups/schedules (okstra-rollup / okstra-schedule-gen), a brief (okstra-brief-gen), setup (okstra-setup), or cross-project management (okstra-manager).
5
5
  ---
6
6
 
7
7
  # OKSTRA Inspect
@@ -18,6 +18,8 @@ Single read-side entry point for okstra runtime inspection plus the one status m
18
18
  | `cost` | `facets/cost.md` | Estimate file/read context cost for a task bundle. |
19
19
  | `errors` | `facets/errors.md` | Aggregate okstra-run error logs for a task into a timestamped markdown report; print a summary. |
20
20
  | `error-zip` | `facets/error-zip.md` | Collect cross-project okstra error logs into an anonymized zip (report + raw) and summarize clusters. |
21
+ | `error-issue` | `facets/error-issue.md` | Turn cross-project okstra anomalies into GitHub issue candidates; file the approved ones after explicit user approval. |
22
+ | `run-audit` | `facets/run-audit.md` | Check every run's artifacts against progress invariants; report what went wrong without an error ever being logged. |
21
23
  | `recap` | `facets/recap.md` | Summarize a task's run-to-run phase transitions, then answer free-form questions over its `.okstra` artifacts. Appends each summary/Q&A to `recap/recap-log.jsonl`, and writes agent-authored notes to `notes/` for feeding into later runs. |
22
24
 
23
25
  ## Step 0: Preflight (shared)
@@ -0,0 +1,77 @@
1
+ # okstra-inspect facet — error-issue
2
+
3
+ Loaded lazily by the dispatch table in `SKILL.md` (core). Shared rules — Step 0 preflight, the standard task-key resolution rule (0/1/N), the no-task fallback, and Output Rules — live in the core file and still apply here.
4
+
5
+ ## error-issue
6
+
7
+ Trigger phrases: "okstra error-issue", "issue candidates", "file okstra issues", "report okstra defects".
8
+
9
+ Turn okstra run anomalies into GitHub issue candidates, then file the approved ones. Read-only over every target's `.okstra/`. Apart from the plan file you edit between the two commands, the only write is the GitHub issue — and that only after the user approves.
10
+
11
+ **Never run `submit` without an explicit user approval in this session.** `plan` is free to run unattended; `submit` is not.
12
+
13
+ ### error-issue.1 — Build the plan
14
+
15
+ ```bash
16
+ okstra error-issue plan --out ~/.okstra/error-issue-plan.json
17
+ ```
18
+
19
+ Parse the stdout JSON: `candidateCount`, `belowGate`, `notOkstraDefect`, `conflicted`, `unreachableRuns`.
20
+
21
+ `candidateCount` includes candidates whose action is already `skip`, so it is not the number of issues that will be filed. Report the filed count from the create/comment rows of the board below.
22
+
23
+ ### error-issue.2 — Review each candidate's classification
24
+
25
+ Read the plan file. For each candidate, verify the recorded `classification` against its `signals` list. The classification was computed by `issue_signals.classify`; your job is to confirm it reads correctly against the raw evidence, not to invent a new one.
26
+
27
+ If a candidate's signals do not support its classification, drop it from the plan file rather than arguing with it in prose. If deciding requires reading the raw `stderrExcerpt`, add an evidence entry and quote the line — an evidence entry always carries all three of `signal`, `value`, `source`, so write `{"signal": "<the signal it backs>", "value": "<the quoted line>", "source": "message excerpt"}`. `submit` rejects an entry missing any of the three; a two-key entry is not a lighter form of evidence, it is an unreadable one.
28
+
29
+ ### error-issue.3 — Present the approval board (Korean)
30
+
31
+ First state the destination on its own line — `plan.json` 의 `repo` 값을 그대로 읽어 `등록 대상: <repo>` 로 적는다. 이 값은 `~/.okstra/error-issue.json` 이 덮어쓸 수 있으므로 okstra 레포라고 단정하지 않는다. CLI 가 `repo` 없는 plan 을 거부하는 이유가 바로 이것 — 목적지는 승인 화면에 반드시 있어야 할 값이다.
32
+
33
+ Then show two tables, never merged into one.
34
+
35
+ **등록 예정** — candidates whose action is `create` or `comment`. `title` 열은 실제로 올라갈 제목 그대로 싣는다. 그것이 없으면 "승인 화면에서 본 것과 올라가는 것이 같다"가 성립하지 않는다:
36
+
37
+ | # | action | title | fingerprint | errorType / phase | 발생 | 지지 신호(`evidence`) | 기존 이슈 |
38
+ |---|---|---|---|---|---:|---|---|
39
+
40
+ **보류** — candidates whose action is already `skip`. They are NOT filed; they are shown so the human sees what was set aside. `skip` has two distinct origins and the row must say which:
41
+
42
+ - **반대 신호** — `conflictingSignals` is non-empty. `classify` called it an okstra defect while another signal pointed elsewhere.
43
+ - **이미 보고됨** — `conflictingSignals` is empty and `existingIssue` is set. An open issue already covers the last occurrence, so there is nothing new to say.
44
+
45
+ | # | fingerprint | errorType / phase | 발생 | 보류 사유 | 지지 신호 | 반대 신호 / 기존 이슈 |
46
+ |---|---|---|---:|---|---|---|
47
+
48
+ **본문** — then, for every `등록 예정` row, print that candidate's `body` **whole and verbatim** in a fenced block, one block per row, labelled with the same `#`. Never elide a section. The table summarizes; the body is what actually gets posted, and "승인 화면에서 본 것과 올라가는 것이 같다" is this facet's whole claim — a title plus a signal list does not carry it.
49
+
50
+ Bodies are bounded by construction: every line is derived (counts, signal values, invariant names) except the one representative message, which shares its source with the `title` in the same row. The raw error records are deliberately NOT in the body — their free text carries the reporting project's ticket ids and source filenames, and no shape rule can separate those from okstra's own vocabulary. They stay in the candidate's `records` in `plan.json` and in the `okstra error-zip` archive. If you need to vet them, read `plan.json` locally; do not paste them into the body.
51
+
52
+ Then ask via `AskUserQuestion` (options: 전부 등록 / 일부만 선택 / 등록 안 함). Deselected candidates are removed from the plan file before submit.
53
+
54
+ A held candidate is promoted only when the user explicitly says so, and the action you set depends on `existingIssue`:
55
+
56
+ - `existingIssue` is null → set `action` to `create`.
57
+ - `existingIssue` is set → set `action` to `comment`. **Never `create` over an existing issue** — that files a duplicate carrying the same `okstra-fingerprint` marker, and from then on `find_existing_issue` matches whichever of the two the list returns first, so every later run reports against an arbitrary one.
58
+
59
+ Say which you set and why.
60
+
61
+ Surface `unreachableRuns > 0` — no silent omission.
62
+
63
+ ### error-issue.4 — Submit
64
+
65
+ Only after an explicit approval:
66
+
67
+ ```bash
68
+ okstra error-issue submit --plan ~/.okstra/error-issue-plan.json
69
+ ```
70
+
71
+ Report `created` / `commented` / `skipped` / `rejected` / `failed`. If `rejected > 0`, print each rejection's `reasons` verbatim — a rejection means the outbound text or the evidence failed the final gate, and it is never something to work around.
72
+
73
+ If `failed > 0`, print each failure's `error` and **do not re-run `submit` on the same plan file**. A failure is a candidate that crashed or whose `gh` call errored; the candidates before it were already filed, and their `action` in the plan file still says `create`. Re-run `error-issue plan` instead — the fingerprint lookup turns the already-filed ones into `comment`/`skip`, which is the only thing that keeps a retry from filing duplicates.
74
+
75
+ ### error-issue.5 — Next step
76
+
77
+ End with: "이 이슈를 실제로 고치려면 `/okstra-brief-gen` 에 이슈 URL 을 주고, okstra 레포에서 `okstra-run --task-type error-analysis` 로 진행하세요."
@@ -0,0 +1,34 @@
1
+ # okstra-inspect facet — run-audit
2
+
3
+ Loaded lazily by the dispatch table in `SKILL.md` (core). Shared rules — Step 0 preflight, the standard task-key resolution rule (0/1/N), the no-task fallback, and Output Rules — live in the core file and still apply here.
4
+
5
+ ## run-audit
6
+
7
+ Trigger phrases: "okstra run-audit", "run audit", "진행 점검", "태스크들 제대로 가고 있나", "silent failures".
8
+
9
+ Check every run's artifacts against progress invariants. This catches what the error log cannot: a run that never logged a failure but still ended wrong. Read-only.
10
+
11
+ ### run-audit.1 — Run the audit
12
+
13
+ ```bash
14
+ okstra run-audit
15
+ ```
16
+
17
+ ### run-audit.2 — Report (Korean)
18
+
19
+ Group the `violations` array by `invariant` and report one section each:
20
+
21
+ | invariant | 위반 태스크 수 | 프로젝트 수 |
22
+ |---|---:|---|
23
+
24
+ Under each, list the affected `taskKey` values with their `detail` and `source`. Always cite `source` — the reader must be able to open the file the verdict came from.
25
+
26
+ ### run-audit.3 — Split the two outcomes
27
+
28
+ State plainly which violations are this task's own state versus a repeated okstra defect:
29
+
30
+ - an invariant broken in **one** task → that task's own state; tell the user what to do about that task
31
+ - an invariant broken across **two or more** tasks → an okstra defect; point at `/okstra-inspect error-issue` to file it
32
+ - `approval-not-forgotten` is the exception and never routes to `error-issue` no matter how far it spreads. It means a human has not approved yet, not that okstra did something wrong — `error_issue.NEVER_AN_ISSUE` drops it, so pointing the user at `error-issue` for it promises a filing that will never happen. Report it as a to-do list of tasks awaiting the user's approval instead.
33
+
34
+ Never file an issue from this facet. Filing is `error-issue`'s job and it has its own approval gate.
@@ -174,7 +174,9 @@ When `next.kind == "done"`, fetch the public wizard outcome:
174
174
  okstra wizard outcome --state-file /var/folders/.../okstra-wizard.AbCd.json
175
175
  ```
176
176
 
177
- Output: `{ok: true, outcome: {renderArgs: {...}, persistActions: [...], confirmationText: "..."}}`.
177
+ Output: `{ok: true, outcome: {renderArgs: {...}, orchestration: {chainStages: "..."}, persistActions: [...], confirmationText: "..."}}`.
178
+
179
+ `renderArgs` holds only flags `okstra render-bundle` accepts, which is what makes the pass-everything rule below safe. `orchestration` holds signals this skill acts on itself — never pass its entries to `render-bundle`.
178
180
 
179
181
  Run every `outcome.persistActions[]` entry BEFORE `render-bundle`. The only supported action is:
180
182
 
@@ -216,6 +218,19 @@ The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.
216
218
 
217
219
  You can delete the literal state-file path after this point — its job is done. Invoke `rm` with the literal path (e.g. `rm /var/folders/.../okstra-wizard.AbCd.json`), not a shell variable.
218
220
 
221
+ <!-- BEGIN FRAGMENT: host-orchestration-implementation -->
222
+ ## Host orchestration rules — implementation
223
+
224
+ These are the rules the **host orchestrator** follows around an `implementation`
225
+ run: when to offer a conformance waiver, what a concurrent-run marker means, how
226
+ to recover a stale stage SHA, and what the chaining queue does when the next
227
+ stage is not ready. They are not lead phase rules — the lead's rules live in
228
+ `prompts/profiles/`.
229
+
230
+ This file is the single source. Two surfaces are generated from it: the
231
+ `okstra-run` skill body (marker block, synced by `tools/sync-skill-fragments.mjs`)
232
+ and each run's `instruction-set/host-orchestration-rules.md`. Edit here.
233
+
219
234
  ### Step 5.1 (implementation only): blocking local conformance waiver offer
220
235
 
221
236
  `render-bundle` accepts an optional `--qa-waiver "<stageKey>:<reason>"` flag (implementation only). It records a **user-acknowledged** waiver into the task-level conformance manifest entry (`entry.waiver`), letting the run proceed when an `io`-only Tier 3 conformance script genuinely cannot run. The waiver records the user's reason **verbatim**.
@@ -256,6 +271,13 @@ If `render-bundle` fails with a `PrepareError` containing `Recorded stage SHAs n
256
271
 
257
272
  If the anchor (`implementation_base_commit`) is reported unresolvable, run the same command's `--reset-anchor <ref>` after user confirmation. Correcting a confirm item without the picker is forbidden — the runtime also rejects a confirm correction without `--use-ref`.
258
273
 
274
+ ### Next stage not yet ready — normal termination (not an exception gate)
275
+ Because of the dependency closure, the chain queue **may include a stage that another implementation run has occupied as started/reserved.** That stage's `render-bundle` is rejected with `--stage N already in progress or reserved by another run` (StageTargetError). This is **not** an exception gate needing human judgment but a "next stage not yet ready" situation. On this rejection, **terminate the chain normally** and report the remaining queue to the user (e.g. `remaining queue: stage 4, 5 — resume with okstra-run after occupancy is released`). This is a different branch from the exception gate below (data corruption·concurrent-occupancy conflict confirmation).
276
+
277
+ ### Exception gate during chaining
278
+ If `render-bundle` raises Step 5's concurrent-run conflict detection (concurrent-run branch) or git stale-SHA reconciliation (git-reconcile branch), **stop the chain at that stage** and present the gate to the user exactly as Step 5 prescribes. Once the user resolves the gate, resume the chain in place (continue with the remaining queue). Data corruption·concurrent-occupancy conflicts are confirmed by a human — this is the safety boundary of unattended chaining. (Unlike the "not ready" rejection above, these two branches do not discard the queue; they wait for user resolution.)
279
+ <!-- END FRAGMENT: host-orchestration-implementation -->
280
+
259
281
  ## Step 6: Take over as Okstra lead
260
282
 
261
283
  Read `<INSTRUCTION_SET_PATH>/lead-execution-prompt.md` verbatim and take over as `Okstra lead` in the current host-native session. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
@@ -265,11 +287,11 @@ Then proceed through the phases exactly as the lead prompt directs (Phase 1 cont
265
287
  Inform the user with one short line:
266
288
  > Took over as Okstra lead (`<host-runtime>`) for `<taskKey>` (`<task-type>`). Run dir: `<RUN_DIR_RELATIVE_PATH>`. Beginning Phase 1 (context loading).
267
289
 
268
- ## Step 7: implementation unattended chaining (chain-stages)
290
+ ## Step 7: implementation unattended chaining (orchestration.chainStages)
269
291
 
270
- When `task-type == implementation` and Step 5 render-args' `chain-stages` CSV has 2+ elements, the current session acts as the orchestrator and runs the stages in dependency order as an unattended chain (a single element behaves like the existing single run, so skip this section — the end of Step 6 is the end of the run).
292
+ When `task-type == implementation` and Step 5 outcome's `orchestration.chainStages` CSV has 2+ elements, the current session acts as the orchestrator and runs the stages in dependency order as an unattended chain (a single element behaves like the existing single run, so skip this section — the end of Step 6 is the end of the run).
271
293
 
272
- Queue = the topologically-sorted stage list from splitting `chain-stages` on `,` (the order Task 5 emitted by topologically sorting the dependency closure). For each stage `N` in the queue, in order:
294
+ Queue = the topologically-sorted stage list from splitting `orchestration.chainStages` on `,` (the order Task 5 emitted by topologically sorting the dependency closure). For each stage `N` in the queue, in order:
273
295
 
274
296
  1. Call Step 5's `render-bundle` with the same arguments but `--stage N` (the base commit is auto-computed by prepare from the predecessor's done `head_commit`, so do not pass it by hand). Step 5's blocking local conformance waiver offer·concurrent-run detection·git-reconcile gates apply identically to each stage's `render-bundle`.
275
297
  2. As in Step 6, become the host-native Okstra lead and run that stage's Phase 1–7 inline. Phase 6's lead post-stage persistence appends that stage's `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` (per the implementation profile directive).
@@ -278,11 +300,7 @@ Queue = the topologically-sorted stage list from splitting `chain-stages` on `,`
278
300
 
279
301
  Once the whole queue is consumed, end the chain and report completion to the user.
280
302
 
281
- ### Next stage not yet ready — normal termination (not an exception gate)
282
- Because of the dependency closure, the chain queue **may include a stage that another implementation run has occupied as started/reserved.** That stage's `render-bundle` is rejected with `--stage N already in progress or reserved by another run` (StageTargetError). This is **not** an exception gate needing human judgment but a "next stage not yet ready" situation. On this rejection, **terminate the chain normally** and report the remaining queue to the user (e.g. `remaining queue: stage 4, 5 — resume with okstra-run after occupancy is released`). This is a different branch from the exception gate below (data corruption·concurrent-occupancy conflict confirmation).
283
-
284
- ### Exception gate during chaining
285
- If `render-bundle` raises Step 5's concurrent-run conflict detection (concurrent-run branch) or git stale-SHA reconciliation (git-reconcile branch), **stop the chain at that stage** and present the gate to the user exactly as Step 5 prescribes. Once the user resolves the gate, resume the chain in place (continue with the remaining queue). Data corruption·concurrent-occupancy conflicts are confirmed by a human — this is the safety boundary of unattended chaining. (Unlike the "not ready" rejection above, these two branches do not discard the queue; they wait for user resolution.)
303
+ The two branches that end or pause the queue — "Next stage not yet ready" and "Exception gate during chaining" — are in the host orchestration rules block above.
286
304
 
287
305
  ## Persisting the PR template scope (release-handoff)
288
306
 
@@ -308,4 +326,4 @@ Do not read the wizard state file directly. `okstra wizard outcome` exposes any
308
326
 
309
327
  - Echo each captured answer (`result.echo`) on one short line so the user sees what was registered.
310
328
  - Never invent identity; if a `text` prompt returns an empty answer where the wizard rejects it, the user must retry.
311
- - 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 `chain-stages` 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.
329
+ - 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.
@@ -1,21 +1,21 @@
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, 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.
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, its options with their impact 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.** 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.
11
+ **Core principle — the skill never picks an answer for the user.** It builds the option board — background, a self-contained question, the report's options with their impact, `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
12
 
13
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.
14
14
 
15
15
  | Sub-command | What it does |
16
16
  |---|---|
17
17
  | `list` | List tasks that still have approval-open clarification, newest report first. |
18
- | `show` | Expand one report's open `C-*` rows (statement + recommended + alternatives + contextRefs). |
18
+ | `show` | Expand one report's open `C-*` rows (statement + options + contextRefs). |
19
19
  | `write` | Record the collected answers (+ optional approval) as a `user-responses/` sidecar. |
20
20
 
21
21
  ## Step 0: Preflight (shared)
@@ -71,7 +71,11 @@ Carry the chosen entry's `reportPath` and `taskKey` forward.
71
71
  okstra user-response show --report <reportPath>
72
72
  ```
73
73
 
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).
74
+ Returns `{reportPath, rows: [{id, kind, blocks, status, statement, expectedForm, options, contextRefs, resolvedRefs}]}`.
75
+
76
+ Each entry of `options[]` is `{role, answer, rationale, scopeImpact, addedWork, directionChange}`. `role` is `recommended` for exactly one entry and `alternative` for the rest; `scopeImpact` is a token list drawn from `in-repo` / `cross-repo` / `new-schema` / `deferrable`; `addedWork` and `directionChange` are one sentence each. A schema-v1 report has nowhere to record impact, so those three arrive empty — the CLI reconstructs only the answers from the report's `Expected form` cell.
77
+
78
+ `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).
75
79
 
76
80
  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
81
 
@@ -103,23 +107,19 @@ Close the background with the raw source on one line, so the mapping back to the
103
107
 
104
108
  > Source: `C-014` — "<raw statement>"
105
109
 
106
- ### 3b. The picker — three answers plus `Enter directly`
110
+ ### 3b. The picker — the report's options plus `Enter directly`
107
111
 
108
- One `AskUserQuestion` (single-select), with exactly four options in this order:
112
+ One `AskUserQuestion` (single-select). Fill the slots from `options[]` in array order — the `role: recommended` entry first with its label suffixed `(Recommended)`, then the `alternative` entries — and always close with `Enter directly` as the last option. Never mark anything but the `recommended` entry as recommended.
109
113
 
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
+ Each `label` is that option's `answer`, kept to the answer itself (1–5 words). Each `description` carries the rationale followed by the three impact axes, in this fixed order:
114
115
 
115
- Slots 2–3 fill from `alternatives[]` first. When `alternatives[]` is short, fill each remaining slot with the first of these that applies:
116
+ > `<rationale>` — Scope: `<scopeImpact, comma-joined>` · Added work: `<addedWork>` · Direction: `<directionChange>`
116
117
 
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.
118
+ The three axes answer three different questions: how far the choice reaches, what new work it creates, and what it overturns. Never fold them into one phrase — whichever is easiest to write ends up standing in for the other two, and the user weighs a scope change as though it were free. That is the failure this board exists to prevent.
119
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.
120
+ When an axis is empty — a schema-v1 report has nowhere to record impact — write `not stated in the report` for that axis. Do not infer it, and do not read the code to reconstruct it. A guessed side effect is worse than a stated gap, because the user cannot tell the two apart.
121
121
 
122
- Keep each `label` to the answer itself (1–5 words); the description carries the consequence.
122
+ When `options[]` carries more than three entries, keep the recommended one plus the two alternatives whose `scopeImpact` differs most from it, and say in the background text how many you left out. When it carries exactly two, the picker has three options in total — do not pad it with an invented third.
123
123
 
124
124
  ### 3c. Transcribe the decision, then move on
125
125
 
@@ -127,13 +127,13 @@ Record one entry — `{id, kind, value, rationale?, disposition}`, `kind` copied
127
127
 
128
128
  | The user picks | `value` | `disposition` |
129
129
  |---|---|---|
130
- | Option 1–3 | that option's full answer text, not its short label | `answer` |
130
+ | One of the `options[]` entries | that option's `answer` text, not its short label | `answer` |
131
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` |
132
+ | Free text asking for the item to be re-asked | what the user wants re-asked, verbatim (empty → the raw statement) | `reframe` |
133
133
 
134
134
  A reframe is not an answer, so it does not satisfy the approval gate.
135
135
 
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.
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 options. Explain only — **do not resolve it for them**; the decision goes back to the user.
137
137
 
138
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.
139
139
 
@@ -406,6 +406,10 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
406
406
  {% endif %}
407
407
  {% if implementationPlanning.planBodyVerification.gateBlockedBy %}- **Blocked by**: {% for cause in implementationPlanning.planBodyVerification.gateBlockedBy %}`{{ cause }}`{% if not loop.last %}, {% endif %}{% endfor %}
408
408
  {% endif %}
409
+ {% if implementationPlanning.planBodyVerification.uniformVerifiers %}- **{{ t("implementationPlanning.planBodyUniformVerifierLabel") }}**: {% for u in implementationPlanning.planBodyVerification.uniformVerifiers %}`{{ u.worker }}` → `{{ u.verdict }}` ({{ u.itemCount }}){% if not loop.last %}, {% endif %}{% endfor %}
410
+
411
+ > {{ t("implementationPlanning.planBodyUniformVerifierLegend") }}
412
+ {% endif %}
409
413
  > {{ t("implementationPlanning.planBodyGateLegend") }}
410
414
  >
411
415
  > {{ t("implementationPlanning.planBodyBlockedByLegend") }}
@@ -38,6 +38,7 @@ svg { max-width: 100%; min-width: 520px; height: auto; }
38
38
  svg .edge { stroke: GrayText; stroke-width: 2; }
39
39
  svg .edge-arrow-head { fill: GrayText; }
40
40
  svg .node rect { fill: color-mix(in srgb, Highlight 14%, Canvas); stroke: Highlight; }
41
+ svg .node-leading rect { fill: color-mix(in srgb, Highlight 32%, Canvas); stroke-width: 2; }
41
42
  svg .node text { fill: CanvasText; font-size: 13px; }
42
43
  table { width: 100%; border-collapse: collapse; margin-top: 1rem; font-size: .9rem; }
43
44
  /* Korean takes a line break between any two syllables, so an auto-layout
@@ -79,7 +80,19 @@ th, td { text-align: left; vertical-align: top; border-bottom: 1px solid color-m
79
80
  .ledger-source { font-size: .9rem; color: GrayText; }
80
81
  .ledger-source > span:first-child::after { content: ":"; }
81
82
  .status { display: inline-block; white-space: nowrap; border-radius: 999px; padding: .1rem .5rem; background: color-mix(in srgb, GrayText 15%, Canvas); }
82
- .status-gap, .status-risk { background: color-mix(in srgb, #d94b4b 18%, Canvas); }
83
+ .status-gap, .status-risk, .status-error, .status-timeout { background: color-mix(in srgb, #d94b4b 18%, Canvas); }
84
+ /* Figures are read by comparing them down the column, which only works when
85
+ the digits line up: tabular-nums stops a 1 from being narrower than a 7, and
86
+ the right edge is the one they share. */
87
+ .run-usage-lede { max-width: 78ch; }
88
+ .figure { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
89
+ /* Four nowrap number columns need almost none of the width, and the default
90
+ even split spent it on them while the agent names wrapped mid-word. */
91
+ [data-report-section="run-usage"] .row-key { width: 45%; }
92
+ .cli-extra { display: block; color: GrayText; font-size: .85em; }
93
+ tfoot th { font-weight: 600; }
94
+ tfoot tr:first-child > * { border-top: 2px solid color-mix(in srgb, CanvasText 30%, transparent); }
95
+ tfoot .grand-total > * { font-weight: 700; }
83
96
  .human-report-footer { width: min(1120px, calc(100% - 2rem)); margin: 0 auto 2rem; display: flex; flex-wrap: wrap; gap: .6rem; }
84
97
  .human-report-footer pre { flex-basis: 100%; white-space: pre-wrap; max-height: 14em; overflow: auto; }
85
98
  fieldset, label { display: block; margin: .7rem 0; }