okstra 0.155.0 → 0.156.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.155.0",
3
+ "version": "0.156.0",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.155.0",
3
- "builtAt": "2026-08-06T15:13:07.664Z",
2
+ "package": "0.156.0",
3
+ "builtAt": "2026-08-07T06:39:03.548Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -110,11 +110,30 @@ def task_id(manifest: Mapping[str, Any]) -> str:
110
110
  raise FinalizeError(f"cannot infer task id from taskKey: {task_key}")
111
111
 
112
112
 
113
- def run_seq(manifest: Mapping[str, Any]) -> str:
113
+ def _category_seq(manifest: Mapping[str, Any], category: str) -> str:
114
114
  seqs = manifest.get("runSequencesByCategory")
115
115
  if not isinstance(seqs, Mapping):
116
116
  raise FinalizeError("run manifest has no runSequencesByCategory object")
117
- return require_string(seqs, "manifests")
117
+ return require_string(seqs, category)
118
+
119
+
120
+ def run_seq(manifest: Mapping[str, Any]) -> str:
121
+ """How many times this task type has run — what a lead event records."""
122
+ return _category_seq(manifest, "manifests")
123
+
124
+
125
+ def report_seq(manifest: Mapping[str, Any]) -> str:
126
+ """The `reports` sequence — the one the final-report filename carries.
127
+
128
+ Categories advance independently (`paths.compute_run_paths`), so a run whose
129
+ predecessor produced no report reuses the free report slot and `run_seq`
130
+ outruns this one. This value reaches `render-report-views --seq`, which
131
+ stamps it into the HTML `runMeta` naming the exported
132
+ `user-response-<task-type>-<seq>.md`; `user_response.write_sidecar` derives
133
+ that same name from the report path, so anything but `reports` makes the two
134
+ answer paths write different files for one report.
135
+ """
136
+ return _category_seq(manifest, "reports")
118
137
 
119
138
 
120
139
  def task_manifest_path(project_root: Path, manifest: Mapping[str, Any]) -> Path:
@@ -204,7 +223,7 @@ class FinalizeContext:
204
223
  task_key=require_string(manifest, "taskKey"),
205
224
  task_type=require_string(manifest, "taskType"),
206
225
  task_group=task_group(manifest),
207
- seq=run_seq(manifest),
226
+ seq=report_seq(manifest),
208
227
  final_status_path=resolve_optional_path(
209
228
  project_root, manifest.get("finalStatusPath")
210
229
  ),
@@ -218,6 +218,103 @@ def parse_stage_map_file(path: Path) -> list[StageMapStage]:
218
218
  return parse_stage_map_text(text, source_plan_path=str(resolved))
219
219
 
220
220
 
221
+ def _require_data_positive_int(
222
+ value: Any, field: str, row_number: int, source_plan_path: str,
223
+ ) -> int:
224
+ if not isinstance(value, int) or isinstance(value, bool) or value < 1:
225
+ raise StageMapError(
226
+ "stage_map",
227
+ f"structured Stage Map row {row_number} has invalid {field} {value!r}",
228
+ source_plan_path,
229
+ )
230
+ return value
231
+
232
+
233
+ def _require_data_text(
234
+ value: Any, field: str, row_number: int, source_plan_path: str,
235
+ ) -> str:
236
+ if not isinstance(value, str) or not value.strip():
237
+ raise StageMapError(
238
+ "stage_map",
239
+ f"structured Stage Map row {row_number} has invalid {field} {value!r}",
240
+ source_plan_path,
241
+ )
242
+ return value
243
+
244
+
245
+ def _parse_data_stage_map_row(
246
+ value: Any, row_number: int, source_plan_path: str,
247
+ ) -> StageMapStage:
248
+ if not isinstance(value, dict):
249
+ raise StageMapError(
250
+ "stage_map",
251
+ f"structured Stage Map row {row_number} must be an object",
252
+ source_plan_path,
253
+ )
254
+ stage_number = _require_data_positive_int(
255
+ value.get("stage"), "stage", row_number, source_plan_path
256
+ )
257
+ step_count = _require_data_positive_int(
258
+ value.get("stepCount"), "stepCount", row_number, source_plan_path
259
+ )
260
+ title = _require_data_text(
261
+ value.get("title"), "title", row_number, source_plan_path
262
+ )
263
+ depends_on = _require_data_text(
264
+ value.get("dependsOn"), "dependsOn", row_number, source_plan_path
265
+ )
266
+ exit_summary = _require_data_text(
267
+ value.get("exitContractSummary"),
268
+ "exitContractSummary",
269
+ row_number,
270
+ source_plan_path,
271
+ )
272
+ return StageMapStage(
273
+ stage_number,
274
+ title,
275
+ _parse_depends_on(depends_on.strip(), row_number, source_plan_path),
276
+ step_count,
277
+ exit_summary,
278
+ )
279
+
280
+
281
+ def _parse_schema_v2_stage_map(
282
+ data: dict[str, Any], source_plan_path: str,
283
+ ) -> list[StageMapStage]:
284
+ planning = data.get("implementationPlanning")
285
+ stage_map = planning.get("stageMap") if isinstance(planning, dict) else None
286
+ if not isinstance(stage_map, list) or not stage_map:
287
+ raise StageMapError(
288
+ "stage_map",
289
+ "structured report requires a non-empty implementationPlanning.stageMap",
290
+ source_plan_path,
291
+ )
292
+ stages = [
293
+ _parse_data_stage_map_row(value, row_number, source_plan_path)
294
+ for row_number, value in enumerate(stage_map, start=1)
295
+ ]
296
+ _validate_stage_numbers(stages, source_plan_path)
297
+ return stages
298
+
299
+
300
+ def _parse_stage_map_source(markdown_path: Path) -> list[StageMapStage]:
301
+ resolved = markdown_path.resolve()
302
+ data_path = resolved.with_suffix(".data.json")
303
+ if not data_path.exists():
304
+ return parse_stage_map_file(resolved)
305
+ try:
306
+ data = json.loads(data_path.read_text(encoding="utf-8"))
307
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
308
+ raise StageMapError("stage_map", str(exc), str(data_path)) from exc
309
+ if not isinstance(data, dict):
310
+ raise StageMapError(
311
+ "stage_map", "structured report must be an object", str(data_path)
312
+ )
313
+ if data.get("schemaVersion") != "2.0":
314
+ return parse_stage_map_file(resolved)
315
+ return _parse_schema_v2_stage_map(data, str(data_path))
316
+
317
+
221
318
  def stage_map_records(stages: Iterable[StageMapStage]) -> list[dict[str, Any]]:
222
319
  return [
223
320
  {
@@ -256,7 +353,7 @@ def load_task_stage_map(
256
353
  return StageMapSnapshot(
257
354
  "ready",
258
355
  str(resolved),
259
- stage_map_records(parse_stage_map_file(resolved)),
356
+ stage_map_records(_parse_stage_map_source(resolved)),
260
357
  )
261
358
 
262
359