okstra 0.142.0 → 0.144.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 (28) hide show
  1. package/docs/project-structure-overview.md +3 -0
  2. package/docs/task-process/error-analysis.md +9 -4
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/agents/workers/report-writer-worker.md +2 -1
  6. package/runtime/prompts/coding-preflight/overview.md +1 -1
  7. package/runtime/prompts/lead/context-loader.md +2 -2
  8. package/runtime/prompts/lead/convergence.md +5 -2
  9. package/runtime/prompts/lead/report-writer.md +4 -3
  10. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  11. package/runtime/prompts/profiles/_common-contract.md +3 -1
  12. package/runtime/prompts/profiles/_implementation-verifier.md +48 -2
  13. package/runtime/prompts/profiles/error-analysis.md +5 -1
  14. package/runtime/python/okstra_ctl/analysis_packet.py +28 -3
  15. package/runtime/python/okstra_ctl/brief_frontmatter.py +56 -0
  16. package/runtime/python/okstra_ctl/convergence_engine.py +66 -15
  17. package/runtime/python/okstra_ctl/mutation_probe.py +1263 -0
  18. package/runtime/python/okstra_ctl/run.py +81 -33
  19. package/runtime/python/okstra_ctl/schema_excerpt.py +5 -3
  20. package/runtime/python/okstra_ctl/self_mock_signals.py +183 -0
  21. package/runtime/python/okstra_ctl/wizard.py +2 -44
  22. package/runtime/schemas/final-report-v1.0.schema.json +99 -0
  23. package/runtime/templates/reports/final-report.template.md +26 -0
  24. package/runtime/templates/reports/i18n/en.json +20 -2
  25. package/runtime/templates/reports/i18n/ko.json +20 -2
  26. package/runtime/validators/detect_self_mock.py +220 -0
  27. package/runtime/validators/validate-brief.py +5 -1
  28. package/runtime/validators/validate-run.py +763 -36
@@ -78,7 +78,9 @@ def seed_working_state(grouped_input: Mapping[str, Any]) -> dict[str, Any]:
78
78
  state["stopReason"] = "auto-disabled"
79
79
  return state
80
80
 
81
- findings, queue = _parse_groups(source.get("groups"), analysis_workers)
81
+ findings, queue = _parse_groups(
82
+ source.get("groups"), analysis_workers, adversarial=config["adversarial"]
83
+ )
82
84
  state["findings"] = findings
83
85
  state["queueFindingIds"] = queue
84
86
  return state
@@ -206,6 +208,37 @@ def classify_adversarial_round(
206
208
  return "partial-consensus"
207
209
 
208
210
 
211
+ def _round_has_counter_evidence(
212
+ votes: Mapping[str, Mapping[str, Any]],
213
+ ) -> bool:
214
+ return any(
215
+ vote.get("verdict") == "disagree"
216
+ and vote.get("disagreeBasis") == "counter-evidence"
217
+ for vote in votes.values()
218
+ if isinstance(vote, Mapping)
219
+ )
220
+
221
+
222
+ def _classify_adversarial_history(
223
+ rounds: list[Mapping[str, Any]],
224
+ ) -> str | None:
225
+ """Classify adversarial rounds without erasing earlier counter-evidence."""
226
+ counter_evidence_seen = False
227
+ for row in rounds:
228
+ votes = row.get("votes") if isinstance(row, Mapping) else None
229
+ if not isinstance(votes, Mapping):
230
+ continue
231
+ counter_evidence_seen = (
232
+ counter_evidence_seen or _round_has_counter_evidence(votes)
233
+ )
234
+ classification = classify_adversarial_round(votes)
235
+ if classification == "worker-unique":
236
+ return classification
237
+ if classification is not None and not counter_evidence_seen:
238
+ return classification
239
+ return None
240
+
241
+
209
242
  def apply_round_results(
210
243
  state: Mapping[str, Any],
211
244
  plan: Mapping[str, Any],
@@ -251,7 +284,7 @@ def apply_round_results(
251
284
  {"round": expected_plan["round"], "votes": round_votes}
252
285
  )
253
286
  classification = (
254
- classify_adversarial_round(round_votes)
287
+ _classify_adversarial_history(finding["rounds"])
255
288
  if adversarial
256
289
  else classify_collaborative_round(round_votes)
257
290
  )
@@ -1321,20 +1354,21 @@ def _expected_final_classification(
1321
1354
  rounds = finding.get("rounds")
1322
1355
  if not isinstance(rounds, list) or not rounds:
1323
1356
  return None
1357
+ if adversarial:
1358
+ try:
1359
+ return _classify_adversarial_history(rounds) or "contested"
1360
+ except ConvergenceContractError:
1361
+ return "contested"
1324
1362
  for row in rounds:
1325
1363
  if not isinstance(row, Mapping) or not isinstance(row.get("votes"), Mapping):
1326
1364
  continue
1327
1365
  try:
1328
- resolved = (
1329
- classify_adversarial_round(row["votes"])
1330
- if adversarial
1331
- else classify_collaborative_round(row["votes"])
1332
- )
1366
+ resolved = classify_collaborative_round(row["votes"])
1333
1367
  except ConvergenceContractError:
1334
1368
  continue
1335
1369
  if resolved is not None:
1336
1370
  return resolved
1337
- return "contested" if adversarial else _final_collaborative_classification(finding)
1371
+ return _final_collaborative_classification(finding)
1338
1372
 
1339
1373
 
1340
1374
  def _validate_round_ledger_counts(
@@ -1347,7 +1381,7 @@ def _validate_round_ledger_counts(
1347
1381
  if not isinstance(history_row, Mapping):
1348
1382
  continue
1349
1383
  ledgers = _round_ledgers(findings, round_number)
1350
- resolved = _resolved_ledger_count(ledgers, adversarial)
1384
+ resolved = _resolved_ledger_count(findings, round_number, adversarial)
1351
1385
  expected = (len(ledgers), resolved, len(ledgers) - resolved)
1352
1386
  actual = (
1353
1387
  history_row.get("inputQueueSize"),
@@ -1381,17 +1415,32 @@ def _round_ledgers(
1381
1415
 
1382
1416
 
1383
1417
  def _resolved_ledger_count(
1384
- ledgers: list[Mapping[str, Any]],
1418
+ findings: list[Any],
1419
+ round_number: int,
1385
1420
  adversarial: bool,
1386
1421
  ) -> int:
1387
1422
  resolved = 0
1388
- for row in ledgers:
1389
- votes = row.get("votes")
1423
+ for finding in findings:
1424
+ rounds = finding.get("rounds") if isinstance(finding, Mapping) else None
1425
+ if not isinstance(rounds, list):
1426
+ continue
1427
+ current_rounds = [
1428
+ row
1429
+ for row in rounds
1430
+ if isinstance(row, Mapping)
1431
+ and isinstance(row.get("round"), int)
1432
+ and row["round"] <= round_number
1433
+ ]
1434
+ current = next(
1435
+ (row for row in current_rounds if row.get("round") == round_number),
1436
+ None,
1437
+ )
1438
+ votes = current.get("votes") if isinstance(current, Mapping) else None
1390
1439
  if not isinstance(votes, Mapping):
1391
1440
  continue
1392
1441
  try:
1393
1442
  classification = (
1394
- classify_adversarial_round(votes)
1443
+ _classify_adversarial_history(current_rounds)
1395
1444
  if adversarial
1396
1445
  else classify_collaborative_round(votes)
1397
1446
  )
@@ -1486,7 +1535,7 @@ def _validate_no_reappearance_after_resolution(
1486
1535
  continue
1487
1536
  try:
1488
1537
  classification = (
1489
- classify_adversarial_round(votes)
1538
+ _classify_adversarial_history(rounds[: index + 1])
1490
1539
  if adversarial
1491
1540
  else classify_collaborative_round(votes)
1492
1541
  )
@@ -1842,6 +1891,8 @@ def _parse_workers(value: Any) -> list[dict[str, str]]:
1842
1891
  def _parse_groups(
1843
1892
  value: Any,
1844
1893
  analysis_workers: list[str],
1894
+ *,
1895
+ adversarial: bool,
1845
1896
  ) -> tuple[list[dict[str, Any]], list[str]]:
1846
1897
  if not isinstance(value, list):
1847
1898
  raise ConvergenceContractError("groups must be an array")
@@ -1855,7 +1906,7 @@ def _parse_groups(
1855
1906
  raise ConvergenceContractError(f"duplicate findingId: {finding_id}")
1856
1907
  seen_ids.add(finding_id)
1857
1908
  finding, source_workers = _parse_group(group, index, analysis_workers)
1858
- if len(source_workers) >= 2:
1909
+ if len(source_workers) >= 2 and not adversarial:
1859
1910
  finding["classification"] = "full-consensus"
1860
1911
  else:
1861
1912
  queue.append(finding_id)