okstra 0.164.0 → 0.165.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.md +12 -8
  3. package/docs/cli.md +7 -3
  4. package/docs/for-ai/README.md +2 -2
  5. package/docs/for-ai/skills/okstra-inspect.md +2 -2
  6. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  7. package/docs/project-structure-overview.md +15 -9
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/workers/antigravity-worker.md +9 -7
  11. package/runtime/agents/workers/codex-worker.md +9 -7
  12. package/runtime/agents/workers/grok-worker.md +6 -4
  13. package/runtime/agents/workers/kimi-worker.md +6 -4
  14. package/runtime/bin/okstra-antigravity-exec.sh +1 -340
  15. package/runtime/bin/okstra-claude-exec.sh +1 -178
  16. package/runtime/bin/okstra-codex-exec.sh +1 -467
  17. package/runtime/bin/okstra-provider-exec.py +165 -190
  18. package/runtime/bin/okstra-trace-cleanup.sh +14 -7
  19. package/runtime/bin/okstra-wrapper-status.py +26 -19
  20. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  21. package/runtime/prompts/lead/convergence.md +36 -8
  22. package/runtime/prompts/lead/okstra-lead-contract.md +23 -1
  23. package/runtime/prompts/lead/plan-body-verification.md +9 -1
  24. package/runtime/prompts/lead/report-writer.md +1 -0
  25. package/runtime/prompts/lead/team-contract.md +3 -3
  26. package/runtime/prompts/profiles/_common-contract.md +9 -1
  27. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  28. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  29. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  30. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  31. package/runtime/prompts/profiles/implementation-planning.md +5 -3
  32. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
  33. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  34. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +148 -0
  35. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
  36. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +41 -0
  37. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +44 -0
  38. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +42 -0
  39. package/runtime/python/okstra_ctl/dispatch_core.py +5 -1
  40. package/runtime/python/okstra_ctl/dispatch_state.py +10 -0
  41. package/runtime/python/okstra_ctl/domain/provider.py +5 -1
  42. package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
  43. package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
  44. package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
  45. package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
  46. package/runtime/python/okstra_ctl/report_html/common.py +71 -25
  47. package/runtime/python/okstra_ctl/report_html/models.py +5 -0
  48. package/runtime/python/okstra_ctl/report_html/render.py +1 -1
  49. package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
  50. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
  51. package/runtime/python/okstra_ctl/report_views.py +44 -16
  52. package/runtime/python/okstra_ctl/stage_citations.py +52 -15
  53. package/runtime/python/okstra_ctl/user_response.py +45 -29
  54. package/runtime/python/okstra_ctl/wizard.py +13 -9
  55. package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
  56. package/runtime/python/okstra_ctl/worker_request.py +140 -0
  57. package/runtime/python/okstra_ctl/worker_runner.py +622 -0
  58. package/runtime/python/okstra_token_usage/collect.py +8 -1
  59. package/runtime/python/okstra_token_usage/report.py +42 -0
  60. package/runtime/python/okstra_token_usage/task_totals.py +88 -0
  61. package/runtime/schemas/final-report-v1.0.schema.json +70 -0
  62. package/runtime/schemas/final-report-v2.0.schema.json +90 -0
  63. package/runtime/skills/okstra-inspect/SKILL.md +1 -2
  64. package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
  65. package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
  66. package/runtime/skills/okstra-run/SKILL.md +1 -1
  67. package/runtime/skills/okstra-user-response/SKILL.md +15 -5
  68. package/runtime/templates/report-writer-prompt-preamble.md +1 -0
  69. package/runtime/templates/reports/html/assets/base.css +8 -4
  70. package/runtime/templates/reports/html/base.template.html +12 -6
  71. package/runtime/templates/reports/html/i18n/en.json +29 -6
  72. package/runtime/templates/reports/html/i18n/ko.json +29 -6
  73. package/runtime/templates/reports/html/macros/forms.html +9 -3
  74. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
  75. package/runtime/templates/reports/report.js +59 -26
  76. package/runtime/templates/reports/user-response.template.md +12 -8
  77. package/runtime/validators/validate-run.py +88 -7
  78. package/runtime/validators/validate_session_conformance.py +62 -1
  79. package/src/cli-registry.mjs +0 -7
  80. package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
  81. package/runtime/python/okstra_ctl/error_issue.py +0 -640
  82. package/runtime/python/okstra_ctl/issue_signals.py +0 -186
  83. package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
  84. package/src/commands/inspect/error-issue.mjs +0 -27
@@ -24,6 +24,8 @@ from okstra_ctl.render_final_report import ( # noqa: E402
24
24
  render_to_file,
25
25
  )
26
26
 
27
+ from .task_totals import task_cumulative_usage # noqa: E402
28
+
27
29
 
28
30
  class SubstituteRefusedError(RuntimeError):
29
31
  """Raised when substitution would write zero-only token cells.
@@ -89,6 +91,8 @@ def _populate_execution_row(row: dict, source: dict) -> None:
89
91
  return
90
92
  if "totalTokens" in usage:
91
93
  row["totalTokens"] = usage["totalTokens"]
94
+ if "cacheReadTokens" in usage:
95
+ row["cacheReadTokens"] = usage["cacheReadTokens"]
92
96
  if "billableEquivalentTokens" in usage:
93
97
  row["billableTokens"] = usage["billableEquivalentTokens"]
94
98
  if "estimatedCostUsd" in usage:
@@ -114,6 +118,7 @@ def _worker_detail_row(label: str, usage: dict) -> dict:
114
118
  return {
115
119
  "label": label,
116
120
  "totalTokens": None,
121
+ "cacheReadTokens": None,
117
122
  "billableTokens": None,
118
123
  "costUsd": None,
119
124
  "cliTotalTokens": None,
@@ -122,6 +127,7 @@ def _worker_detail_row(label: str, usage: dict) -> dict:
122
127
  return {
123
128
  "label": label,
124
129
  "totalTokens": usage.get("totalTokens"),
130
+ "cacheReadTokens": usage.get("cacheReadTokens"),
125
131
  "billableTokens": usage.get("billableEquivalentTokens"),
126
132
  "costUsd": usage.get("estimatedCostUsd"),
127
133
  "cliTotalTokens": usage.get("cliTotalTokens"),
@@ -129,6 +135,35 @@ def _worker_detail_row(label: str, usage: dict) -> dict:
129
135
  }
130
136
 
131
137
 
138
+ def _cache_reads(summary: dict, team_state: dict) -> dict[str, int]:
139
+ """Lead / worker / grand cache-read totals for this run.
140
+
141
+ The figure joined `usageSummary` after most team-states on disk were
142
+ written, and re-rendering one of those reports is exactly when the reader
143
+ goes looking for the corrected table — so a state without the summary keys
144
+ is added up from the usage blocks it does carry.
145
+ """
146
+ lead = summary.get("leadCacheReadTokens")
147
+ worker = summary.get("workerCacheReadTokens")
148
+ if isinstance(lead, int) and isinstance(worker, int):
149
+ return {"lead": lead, "worker": worker, "grand": lead + worker}
150
+ lead = _cache_read_tokens(team_state.get("leadUsage"))
151
+ worker = sum(
152
+ _cache_read_tokens(entry.get("usage"))
153
+ for entry in team_state.get("workers") or []
154
+ if isinstance(entry, dict)
155
+ )
156
+ worker += _cache_read_tokens(summary.get("unattributedWorkerUsage"))
157
+ return {"lead": lead, "worker": worker, "grand": lead + worker}
158
+
159
+
160
+ def _cache_read_tokens(usage: object) -> int:
161
+ if not isinstance(usage, dict):
162
+ return 0
163
+ value = usage.get("cacheReadTokens")
164
+ return value if isinstance(value, int) else 0
165
+
166
+
132
167
  def _worker_detail_rows(team_state: dict) -> list[dict]:
133
168
  rows = []
134
169
  for worker in team_state.get("workers") or []:
@@ -178,19 +213,23 @@ def populate_data_token_cells(data_path: Path, team_state: dict) -> int:
178
213
  worker_cost = cost.get("claudeWorkers") or 0
179
214
  cli_cost = cost.get("cliWorkers") or 0
180
215
 
216
+ cache_reads = _cache_reads(summary, team_state)
181
217
  token_usage = data.setdefault("tokenUsage", {})
182
218
  token_usage.setdefault("lead", {}).update({
183
219
  "totalTokens": summary.get("leadTotalTokens"),
220
+ "cacheReadTokens": cache_reads["lead"],
184
221
  "billableTokens": summary.get("leadBillableEquivalentTokens"),
185
222
  "costUsd": lead_cost,
186
223
  })
187
224
  token_usage.setdefault("worker", {}).update({
188
225
  "totalTokens": summary.get("workerTotalTokens"),
226
+ "cacheReadTokens": cache_reads["worker"],
189
227
  "billableTokens": summary.get("workerBillableEquivalentTokens"),
190
228
  "costUsd": worker_cost,
191
229
  })
192
230
  token_usage.setdefault("grand", {}).update({
193
231
  "totalTokens": summary.get("grandTotalTokens"),
232
+ "cacheReadTokens": cache_reads["grand"],
194
233
  "billableTokens": summary.get("grandBillableEquivalentTokens"),
195
234
  # CLI tracked on its own row per the template — grand here means
196
235
  # lead + claudeWorkers, not lead + claudeWorkers + cli.
@@ -198,6 +237,9 @@ def populate_data_token_cells(data_path: Path, team_state: dict) -> int:
198
237
  })
199
238
  token_usage.setdefault("cli", {})["costUsd"] = cli_cost
200
239
  token_usage["workerDetails"] = _worker_detail_rows(team_state)
240
+ cumulative = task_cumulative_usage(data_path)
241
+ if cumulative is not None:
242
+ token_usage["taskCumulative"] = cumulative
201
243
  changes += 4
202
244
 
203
245
  # Execution Status by Agent — per-row token / cost / duration.
@@ -0,0 +1,88 @@
1
+ """What a whole task has spent, across every run of every phase.
2
+
3
+ A report's own figures cover one run, but a task reaches its plan through
4
+ several: the first pass, a clarification re-run, a conformance fix. Each of
5
+ those bills separately, and a reader who only ever sees the last one has no way
6
+ to learn what the task cost. The per-run team-states are the only record of the
7
+ earlier passes, so the total is summed from them here rather than carried
8
+ forward inside any single run's state.
9
+
10
+ Every team-state is read directly rather than through its `usageSummary`: the
11
+ summary predates the cache-read figure, so an older run would drop out of that
12
+ column while still counting in the others.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+
19
+ _TEAM_STATE_GLOB = "runs/*/state/team-state-*.json"
20
+ _SUMMED_KEYS = (
21
+ ("totalTokens", "totalTokens"),
22
+ ("cacheReadTokens", "cacheReadTokens"),
23
+ ("billableTokens", "billableEquivalentTokens"),
24
+ ("costUsd", "estimatedCostUsd"),
25
+ )
26
+
27
+
28
+ def _task_root(data_path: Path) -> Path | None:
29
+ """The task-bundle directory a report lives under.
30
+
31
+ Report path shape: `<task-id>/runs/<task-type>/reports/<name>.data.json`.
32
+ A path that does not match is not inside a task bundle — a fixture under a
33
+ tmp dir, most often — and has no sibling runs to total.
34
+ """
35
+ parents = data_path.parents
36
+ if len(parents) < 4:
37
+ return None
38
+ if parents[0].name != "reports" or parents[2].name != "runs":
39
+ return None
40
+ return parents[3]
41
+
42
+
43
+ def _usage_blocks(team_state: dict) -> list[dict]:
44
+ blocks = [team_state.get("leadUsage") or {}]
45
+ for worker in team_state.get("workers") or []:
46
+ if isinstance(worker, dict):
47
+ blocks.append(worker.get("usage") or {})
48
+ unattributed = (team_state.get("usageSummary") or {}).get("unattributedWorkerUsage")
49
+ if isinstance(unattributed, dict):
50
+ blocks.append(unattributed)
51
+ return [block for block in blocks if isinstance(block, dict)]
52
+
53
+
54
+ def _number(value: object) -> int | float:
55
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
56
+ return 0
57
+ return value
58
+
59
+
60
+ def task_cumulative_usage(data_path: Path) -> dict | None:
61
+ """Sum every measured run of this report's task, or None when there is none.
62
+
63
+ A run whose collector found no session contributes zero tokens and is left
64
+ out of `runCount` — counting it would make the average per run look cheaper
65
+ than the runs that were actually measured.
66
+ """
67
+ root = _task_root(data_path)
68
+ if root is None:
69
+ return None
70
+ totals = {name: 0 for name, _ in _SUMMED_KEYS}
71
+ run_count = 0
72
+ for state_path in sorted(root.glob(_TEAM_STATE_GLOB)):
73
+ try:
74
+ team_state = json.loads(state_path.read_text(encoding="utf-8"))
75
+ except (OSError, ValueError):
76
+ continue
77
+ if not isinstance(team_state, dict):
78
+ continue
79
+ blocks = _usage_blocks(team_state)
80
+ measured = sum(_number(block.get("totalTokens")) for block in blocks)
81
+ if measured <= 0:
82
+ continue
83
+ run_count += 1
84
+ for name, source_key in _SUMMED_KEYS:
85
+ totals[name] += sum(_number(block.get(source_key)) for block in blocks)
86
+ if not run_count:
87
+ return None
88
+ return {**totals, "runCount": run_count}
@@ -398,6 +398,9 @@
398
398
  ]
399
399
  }
400
400
  }
401
+ },
402
+ "taskCumulative": {
403
+ "$ref": "#/$defs/TokenUsageTaskCumulative"
401
404
  }
402
405
  }
403
406
  },
@@ -3322,6 +3325,17 @@
3322
3325
  }
3323
3326
  ]
3324
3327
  },
3328
+ "cacheReadTokens": {
3329
+ "oneOf": [
3330
+ {
3331
+ "type": "integer",
3332
+ "minimum": 0
3333
+ },
3334
+ {
3335
+ "type": "null"
3336
+ }
3337
+ ]
3338
+ },
3325
3339
  "billableTokens": {
3326
3340
  "oneOf": [
3327
3341
  {
@@ -3399,6 +3413,17 @@
3399
3413
  }
3400
3414
  ]
3401
3415
  },
3416
+ "cacheReadTokens": {
3417
+ "oneOf": [
3418
+ {
3419
+ "type": "integer",
3420
+ "minimum": 0
3421
+ },
3422
+ {
3423
+ "type": "null"
3424
+ }
3425
+ ]
3426
+ },
3402
3427
  "billableTokens": {
3403
3428
  "oneOf": [
3404
3429
  {
@@ -3450,6 +3475,17 @@
3450
3475
  }
3451
3476
  ]
3452
3477
  },
3478
+ "cacheReadTokens": {
3479
+ "oneOf": [
3480
+ {
3481
+ "type": "integer",
3482
+ "minimum": 0
3483
+ },
3484
+ {
3485
+ "type": "null"
3486
+ }
3487
+ ]
3488
+ },
3453
3489
  "billableTokens": {
3454
3490
  "oneOf": [
3455
3491
  {
@@ -6287,6 +6323,40 @@
6287
6323
  }
6288
6324
  }
6289
6325
  ]
6326
+ },
6327
+ "TokenUsageTaskCumulative": {
6328
+ "type": "object",
6329
+ "description": "Every measured run of this task added up — this report's run plus every earlier one. Written by the Phase 7 token-usage step from the per-run team-states.",
6330
+ "required": [
6331
+ "totalTokens",
6332
+ "cacheReadTokens",
6333
+ "billableTokens",
6334
+ "costUsd",
6335
+ "runCount"
6336
+ ],
6337
+ "additionalProperties": false,
6338
+ "properties": {
6339
+ "totalTokens": {
6340
+ "type": "integer",
6341
+ "minimum": 0
6342
+ },
6343
+ "cacheReadTokens": {
6344
+ "type": "integer",
6345
+ "minimum": 0
6346
+ },
6347
+ "billableTokens": {
6348
+ "type": "integer",
6349
+ "minimum": 0
6350
+ },
6351
+ "costUsd": {
6352
+ "type": "number",
6353
+ "minimum": 0
6354
+ },
6355
+ "runCount": {
6356
+ "type": "integer",
6357
+ "minimum": 1
6358
+ }
6359
+ }
6290
6360
  }
6291
6361
  }
6292
6362
  }
@@ -368,6 +368,9 @@
368
368
  ]
369
369
  }
370
370
  }
371
+ },
372
+ "taskCumulative": {
373
+ "$ref": "#/$defs/TokenUsageTaskCumulative"
371
374
  }
372
375
  }
373
376
  },
@@ -3435,6 +3438,20 @@
3435
3438
  "deferrable"
3436
3439
  ]
3437
3440
  },
3441
+ "ClarificationOrigin": {
3442
+ "enum": [
3443
+ "worker-finding",
3444
+ "material-gap",
3445
+ "lead-directed"
3446
+ ]
3447
+ },
3448
+ "ClarificationUserConfirmation": {
3449
+ "enum": [
3450
+ "asked-and-answered",
3451
+ "asked-awaiting",
3452
+ "deferred-no-interactive-session"
3453
+ ]
3454
+ },
3438
3455
  "ClarificationOption": {
3439
3456
  "type": "object",
3440
3457
  "required": [
@@ -5380,6 +5397,17 @@
5380
5397
  }
5381
5398
  ]
5382
5399
  },
5400
+ "cacheReadTokens": {
5401
+ "oneOf": [
5402
+ {
5403
+ "type": "integer",
5404
+ "minimum": 0
5405
+ },
5406
+ {
5407
+ "type": "null"
5408
+ }
5409
+ ]
5410
+ },
5383
5411
  "billableTokens": {
5384
5412
  "oneOf": [
5385
5413
  {
@@ -5457,6 +5485,17 @@
5457
5485
  }
5458
5486
  ]
5459
5487
  },
5488
+ "cacheReadTokens": {
5489
+ "oneOf": [
5490
+ {
5491
+ "type": "integer",
5492
+ "minimum": 0
5493
+ },
5494
+ {
5495
+ "type": "null"
5496
+ }
5497
+ ]
5498
+ },
5460
5499
  "billableTokens": {
5461
5500
  "oneOf": [
5462
5501
  {
@@ -5508,6 +5547,17 @@
5508
5547
  }
5509
5548
  ]
5510
5549
  },
5550
+ "cacheReadTokens": {
5551
+ "oneOf": [
5552
+ {
5553
+ "type": "integer",
5554
+ "minimum": 0
5555
+ },
5556
+ {
5557
+ "type": "null"
5558
+ }
5559
+ ]
5560
+ },
5511
5561
  "billableTokens": {
5512
5562
  "oneOf": [
5513
5563
  {
@@ -8249,6 +8299,12 @@
8249
8299
  "userInput": {
8250
8300
  "type": "string"
8251
8301
  },
8302
+ "origin": {
8303
+ "$ref": "#/$defs/ClarificationOrigin"
8304
+ },
8305
+ "userConfirmation": {
8306
+ "$ref": "#/$defs/ClarificationUserConfirmation"
8307
+ },
8252
8308
  "options": {
8253
8309
  "type": "array",
8254
8310
  "minItems": 2,
@@ -8371,6 +8427,40 @@
8371
8427
  }
8372
8428
  }
8373
8429
  ]
8430
+ },
8431
+ "TokenUsageTaskCumulative": {
8432
+ "type": "object",
8433
+ "description": "Every measured run of this task added up — this report's run plus every earlier one. Written by the Phase 7 token-usage step from the per-run team-states.",
8434
+ "required": [
8435
+ "totalTokens",
8436
+ "cacheReadTokens",
8437
+ "billableTokens",
8438
+ "costUsd",
8439
+ "runCount"
8440
+ ],
8441
+ "additionalProperties": false,
8442
+ "properties": {
8443
+ "totalTokens": {
8444
+ "type": "integer",
8445
+ "minimum": 0
8446
+ },
8447
+ "cacheReadTokens": {
8448
+ "type": "integer",
8449
+ "minimum": 0
8450
+ },
8451
+ "billableTokens": {
8452
+ "type": "integer",
8453
+ "minimum": 0
8454
+ },
8455
+ "costUsd": {
8456
+ "type": "number",
8457
+ "minimum": 0
8458
+ },
8459
+ "runCount": {
8460
+ "type": "integer",
8461
+ "minimum": 1
8462
+ }
8463
+ }
8374
8464
  }
8375
8465
  }
8376
8466
  }
@@ -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 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).
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), 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,7 +18,6 @@ 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
21
  | `run-audit` | `facets/run-audit.md` | Check every run's artifacts against progress invariants; report what went wrong without an error ever being logged. |
23
22
  | `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. |
24
23
 
@@ -6,17 +6,17 @@ Loaded lazily by the dispatch table in `SKILL.md` (core). Shared rules — Step
6
6
 
7
7
  Trigger phrases: "okstra logs", "log status", "log files", "log size", "log cleanup".
8
8
 
9
- Read-only inventory of codex/antigravity wrapper log files written next to each prompt history file (`<prompt>.log`). Reports sizes, ages, totals, and suggests cleanup commands. **Does not delete** — the user runs whichever `find … -delete` line they like.
9
+ Read-only inventory of the worker log files written next to each prompt history file (`<prompt>.log`). Reports sizes, ages, totals, and suggests cleanup commands. **Does not delete** — the user runs whichever `find … -delete` line they like.
10
10
 
11
- **Background:** codex/antigravity wrappers (`okstra-codex-exec.sh`, `okstra-antigravity-exec.sh`) write a sidecar log next to each prompt history file:
11
+ **Background:** every CLI-worker dispatch writes a sidecar log next to its prompt history file. All five providers go through the same shared runner (`~/.okstra/lib/python/okstra_ctl/worker_runner.py`), so the file is there whichever provider ran:
12
12
 
13
13
  ```
14
14
  .okstra/tasks/<task-group>/<task-id>/runs/<phase>/prompts/
15
15
  <worker>-worker-prompt-<phase>-<seq>.md <-- prompt (git-tracked)
16
- <worker>-worker-prompt-<phase>-<seq>.log <-- live stdout+stderr mirror
16
+ <worker>-worker-prompt-<phase>-<seq>.log <-- the run's archive of what the worker did
17
17
  ```
18
18
 
19
- The log is truncated at each dispatch (`: > "$log_path"`) — only the latest run for a given seq is preserved. Different seqs keep separate files. Long-running implementation dispatches can produce multi-MB logs; analysis-phase dispatches are typically smaller.
19
+ The log is rewritten from scratch at each dispatch — only the latest run for a given seq is preserved. Different seqs keep separate files. It holds one row per tool call and per tool result plus the worker's closing text, and drops thinking; a provider that emits plain text instead of an event stream has its progress copy capped at 5000 lines per run, with `[okstra log-cap] N progress line(s) elided` marking what was dropped. Long-running implementation dispatches can still produce multi-MB logs; analysis-phase dispatches are typically smaller.
20
20
 
21
21
  ### logs.1 — Inventory
22
22
 
@@ -29,7 +29,7 @@ Scans `<projectRoot>/.okstra/tasks/**/runs/*/prompts/*.log` and parses task/phas
29
29
  - `perTask[]` — `{taskKey, fileCount, totalBytes, oldestEpoch, newestEpoch}`, total-size desc
30
30
  - `totals` — `{fileCount, totalBytes, taskCount}`
31
31
 
32
- If `totals.fileCount` is 0, report `No wrapper log files found under <projectRoot>` and stop.
32
+ If `totals.fileCount` is 0, report `No worker log files found under <projectRoot>` and stop.
33
33
 
34
34
  ### logs.2 — Summary tables
35
35
 
@@ -28,7 +28,7 @@ Under each, list the affected `taskKey` values with their `detail` and `source`.
28
28
  State plainly which violations are this task's own state versus a repeated okstra defect:
29
29
 
30
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.
31
+ - an invariant broken across **two or more** tasks → a repeated okstra defect; say so and cite every affected `taskKey` so the user can decide what to do with it
32
+ - `approval-not-forgotten` is the exception no matter how far it spreads. It means a human has not approved yet, not that okstra did something wrong. Report it as a to-do list of tasks awaiting the user's approval instead.
33
33
 
34
- Never file an issue from this facet. Filing is `error-issue`'s job and it has its own approval gate.
34
+ This facet reports; it never files anything anywhere.
@@ -183,7 +183,7 @@ That is the entire interactive flow. The wizard handles:
183
183
  - brief path — **asked only for entry task-types (requirements-discovery / error-analysis / improvement-discovery / project-analysis / feature-analysis / change-impact-analysis)** (same-group `.okstra/briefs/<task-group>/**/*.md` candidates first, sorted by the newer of file-created/modified time and latest task-catalog use; direct input last; `Keep / Change` for existing entry tasks). `project-analysis`, `feature-analysis`, and `change-impact-analysis` are brief entry task types. A downstream lifecycle task-type auto carries in the manifest's brief, and when no registered brief exists a `brief_carry` 3-option prompt appears (recommend switching to entry / Enter directly / Abort). `release-handoff` has no brief step at all — prepare generates the input document that cites the verification report,
184
184
  - analysis-input sub-flow — `project-analysis` has no target or evidence step. `feature-analysis` runs `project_evidence_pick` / `project_evidence`, then `analysis_target_pick` / `analysis_target`; the target is required even when the user skips project evidence. `change-impact-analysis` runs `feature_evidence_pick` / `feature_evidence`, then `project_evidence_pick` / `project_evidence`. The evidence steps show accepted compatible reports first and retain direct-path entry; do not invent a different relation or reorder these steps,
185
185
  - base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
186
- - `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = the earliest incomplete stage whose dependencies are satisfied, or a specific stage number) + executor pick. When an approved plan is selected and a `## APPROVAL` sidecar exported from the report — matching the plan on source-report·seq — is detected in that run's sibling `user-responses/`, the approve-confirm step expands to 3 options (`yes_apply` recommended: approve + apply the option as exported / `yes` approve only / `no` abort) — `yes_apply` validates the option against the plan's `optionCandidates` before applying it via the existing approval·option path,
186
+ - `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = the earliest incomplete stage whose dependencies are satisfied, or a specific stage number) + executor pick. When an approved plan is selected and a `## PLAN DECISION` sidecar carrying `Status: approved`, exported from the report — matching the plan on source-report·seq — is detected in that run's sibling `user-responses/`, the approve-confirm step expands to 3 options (`yes_apply` recommended: approve + apply the option as exported / `yes` approve only / `no` abort) — `yes_apply` validates the option against the plan's `optionCandidates` before applying it via the existing approval·option path,
187
187
  - `release-handoff`-only sub-flow: after the approved plan auto-resolves, a `handoff_stage_pick` multi-select — choose an eligible stage bundle (stage-group) or the whole task (when an accepted whole-task verification report exists); the result goes out as render-args' `stages` key (csv, empty when whole-task),
188
188
  - `Use defaults / Customize` branch with profile-aware worker/model questions,
189
189
  - **resume-clarification (in-session equivalent)** — there is no separate mode or flag matching the shell's `okstra.sh --resume-clarification`; two steps of the standard flow carry out its substance. (1) `reuse_previous` (yes/no to reuse the previous run's settings — in `requirements-discovery` / `error-analysis` / `implementation-planning`, only when prior run-inputs exist): YES prefills workers·model·directive·related-tasks at once. (2) `clarification_pick`: if the **task-type's own** previous `final-report` exists it is auto-recommended as the carry-in input (falling back to the newest by mtime across all phases when absent), and the same run's `user-responses/` sidecar (answers the user filled in) is attached alongside. The chosen path is passed to prepare as `--clarification-response` — the user makes the sidecar via the report's `Export user response`, places it in `runs/<task-type>/user-responses/`, and re-runs the same phase,
@@ -147,26 +147,36 @@ Once the loop has covered every open item, **before** calling `write`, **echo ba
147
147
  - If the user changes any item, show the echo-back again with the changed value and re-confirm.
148
148
  - Every `value` must be what the user picked or typed, never a wording you settled on for them — this gate guarantees that.
149
149
 
150
- ## Step 5: (Optional) Record approval
150
+ ## Step 5: (Optional) Record the plan decision
151
151
 
152
- Record approval alongside only when the report's approval-blocking clarification items are **all filled with an answer** and the user explicitly said "approve with this option":
152
+ Record a decision alongside the answers only when the user stated one outright. Never infer it from the tone of the conversation.
153
+
154
+ Approval also requires that the report's approval-blocking clarification items are **all filled with an answer**:
153
155
 
154
156
  ```
155
- --approval '{"approved":true,"implementationOption":"<the option the user chose>"}'
157
+ --plan-decision '{"status":"approved","implementationOption":"<the option the user chose>"}'
156
158
  ```
157
159
 
158
160
  If any approval-target item is not filled with an answer, or is a reframe/hold, do not approve — tell the user the gate is still open.
159
161
 
162
+ A user who turns the plan down gets the same treatment, and the reason is mandatory — the next planning run reads it to know what to change:
163
+
164
+ ```
165
+ --plan-decision '{"status":"rejected","reason":"<the user's own words>"}'
166
+ ```
167
+
168
+ Use `revision-requested` instead of `rejected` when the user wants the same plan reworked rather than dropped. Either way, quote the user's reason; do not summarise it into your own.
169
+
160
170
  ## Step 6: Write the sidecar
161
171
 
162
172
  Run only after the user has confirmed the echo-back with "confirmed":
163
173
 
164
174
  ```bash
165
- okstra user-response write --report <reportPath> --answers '<answers-json>' --approval '<approval-json>' --task-key <taskKey>
175
+ okstra user-response write --report <reportPath> --answers '<answers-json>' --plan-decision '<plan-decision-json>' --task-key <taskKey>
166
176
  ```
167
177
 
168
178
  - `--answers` = the JSON array of `{id, kind, value, rationale?, disposition}` objects collected in Step 3.
169
- - `--approval` is attached only when recording approval in Step 5 (otherwise omit it).
179
+ - `--plan-decision` is attached only when recording a decision in Step 5 (otherwise omit it).
170
180
  - `--task-key` is the value carried forward from Step 1/2.
171
181
  - Report the path from the returned JSON `{sidecar: <path>}` to the user. (`write` merges items when a sidecar already exists — the same `id` is overwritten with the new value.)
172
182
 
@@ -25,6 +25,7 @@ Write `- PROGRESS: <stage> <ISO-8601-UTC>` there before reading and at least eve
25
25
  - Follow the task-type schema excerpt and Phase 6 report-writer contract. Do not perform independent analysis, edit source code, or load implementation coding-preflight resources.
26
26
  - Invoke `okstra render-final-report <Result Path>` after writing data.json and verify the markdown sibling exists before returning.
27
27
  - Preserve source item IDs, convergence classifications, round history, and unresolved dissent; do not recompute them from intuition.
28
+ - Every `clarificationItems[]` row you leave `status: open` with `blocks: approval` carries `origin` and `userConfirmation`. `origin` is `worker-finding` only when an analyser or verifier reached it on its own evidence — when the lead's dispatch prompt told you to raise it, it is `lead-directed`, whatever the workers then agreed. `userConfirmation` is what the lead reports having done about it (`asked-and-answered` / `asked-awaiting` / `deferred-no-interactive-session`); if the dispatch does not say, write `asked-awaiting` and name the gap in your return message rather than guessing. An open approval blocker stops the whole task, so a row that misreports where it came from costs a re-run. **Enforced:** `validators/validate-run.py` `_validate_open_approval_blocker_provenance`.
28
29
 
29
30
  ## Anchor headers
30
31
 
@@ -77,8 +77,9 @@ th, td { text-align: left; vertical-align: top; border-bottom: 1px solid color-m
77
77
  .ledger-key { display: flex; flex-wrap: wrap; gap: .6rem; font-weight: 600; }
78
78
  .ledger-kind { font-weight: 400; color: GrayText; }
79
79
  .ledger-text { max-width: 78ch; }
80
- .ledger-source { font-size: .9rem; color: GrayText; }
81
- .ledger-source > span:first-child::after { content: ":"; }
80
+ .ledger-source, .ledger-confidence { font-size: .9rem; color: GrayText; }
81
+ .ledger-source > span:first-child::after,
82
+ .ledger-confidence > span:first-child::after { content: ":"; }
82
83
  .status { display: inline-block; white-space: nowrap; border-radius: 999px; padding: .1rem .5rem; background: color-mix(in srgb, GrayText 15%, Canvas); }
83
84
  .status-gap, .status-risk, .status-error, .status-timeout { background: color-mix(in srgb, #d94b4b 18%, Canvas); }
84
85
  /* Figures are read by comparing them down the column, which only works when
@@ -86,13 +87,16 @@ th, td { text-align: left; vertical-align: top; border-bottom: 1px solid color-m
86
87
  the right edge is the one they share. */
87
88
  .run-usage-lede { max-width: 78ch; }
88
89
  .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
+ /* Five nowrap number columns need almost none of the width, and the default
90
91
  even split spent it on them while the agent names wrapped mid-word. */
91
- [data-report-section="run-usage"] .row-key { width: 45%; }
92
+ [data-report-section="run-usage"] .row-key { width: 40%; }
92
93
  .cli-extra { display: block; color: GrayText; font-size: .85em; }
93
94
  tfoot th { font-weight: 600; }
94
95
  tfoot tr:first-child > * { border-top: 2px solid color-mix(in srgb, CanvasText 30%, transparent); }
95
96
  tfoot .grand-total > * { font-weight: 700; }
97
+ /* The task total counts runs this report is not about, so it is ruled off from
98
+ the run's own figures rather than reading as one more line of them. */
99
+ tfoot .task-cumulative > * { border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent); color: GrayText; }
96
100
  .human-report-footer { width: min(1120px, calc(100% - 2rem)); margin: 0 auto 2rem; display: flex; flex-wrap: wrap; gap: .6rem; }
97
101
  .human-report-footer pre { flex-basis: 100%; white-space: pre-wrap; max-height: 14em; overflow: auto; }
98
102
  fieldset, label { display: block; margin: .7rem 0; }