okstra 0.139.0 → 0.141.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 (42) hide show
  1. package/README.md +1 -1
  2. package/docs/architecture.md +1 -1
  3. package/docs/cli.md +1 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/agents/workers/report-writer-worker.md +4 -2
  7. package/runtime/prompts/coding-preflight/clean-code.md +55 -0
  8. package/runtime/prompts/coding-preflight/overview.md +3 -2
  9. package/runtime/prompts/launch.template.md +7 -5
  10. package/runtime/prompts/lead/convergence.md +22 -0
  11. package/runtime/prompts/lead/plan-body-verification.md +45 -6
  12. package/runtime/prompts/lead/report-writer.md +2 -0
  13. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -2
  14. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  15. package/runtime/prompts/profiles/error-analysis.md +1 -0
  16. package/runtime/prompts/profiles/final-verification.md +1 -1
  17. package/runtime/prompts/profiles/implementation-planning.md +6 -4
  18. package/runtime/prompts/profiles/improvement-discovery.md +2 -1
  19. package/runtime/prompts/profiles/requirements-discovery.md +2 -1
  20. package/runtime/python/okstra_ctl/build_tools.py +95 -0
  21. package/runtime/python/okstra_ctl/incremental_scope.py +95 -0
  22. package/runtime/python/okstra_ctl/render.py +26 -11
  23. package/runtime/python/okstra_ctl/render_final_report.py +24 -1
  24. package/runtime/python/okstra_ctl/rollup.py +49 -0
  25. package/runtime/python/okstra_ctl/scope_provenance.py +98 -1
  26. package/runtime/python/okstra_ctl/stage_citations.py +47 -0
  27. package/runtime/python/okstra_ctl/workflow.py +7 -4
  28. package/runtime/schemas/final-report-v1.0.schema.json +60 -1
  29. package/runtime/skills/okstra-brief-gen/SKILL.md +38 -11
  30. package/runtime/skills/okstra-code-review/SKILL.md +1 -1
  31. package/runtime/skills/okstra-rollup/SKILL.md +3 -2
  32. package/runtime/templates/reports/brief.template.md +42 -11
  33. package/runtime/templates/reports/fan-out-unit.template.md +9 -4
  34. package/runtime/templates/reports/final-report.template.md +20 -0
  35. package/runtime/templates/reports/final-verification-input.template.md +4 -2
  36. package/runtime/templates/reports/i18n/en.json +11 -4
  37. package/runtime/templates/reports/i18n/ko.json +11 -4
  38. package/runtime/templates/reports/improvement-discovery-input.template.md +2 -2
  39. package/runtime/validators/validate-brief.py +171 -26
  40. package/runtime/validators/validate-run.py +450 -78
  41. package/runtime/validators/validate_fanout.py +19 -10
  42. package/runtime/validators/validate_improvement_report.py +17 -4
@@ -37,6 +37,11 @@ Checks performed per brief file:
37
37
  columns and relation/direction values from the okstra-brief-gen contract.
38
38
  13. The requirement/objective section `## Desired Outcome` (required in every
39
39
  brief variant) exists and its body is not blank. `_(none)_` stays valid.
40
+ 14. The end-state sections `## Expected Behavior` (EB-NNN) / `## Preserved
41
+ Behavior` (PB-NNN) / `## Expected Outcome` (EO-NNN): each item carries a
42
+ 3-digit id and an `— verify: <how>` observation method, EB/EO are rejected
43
+ in the codebase-scan variant, and `## External Gates` — the destination for
44
+ an item with no observation method — is required in every variant.
40
45
 
41
46
  Exit code 0 on PASS, 1 on FAIL.
42
47
  """
@@ -426,42 +431,182 @@ def check_requirement_section(text: str, errors: list[str]) -> None:
426
431
  )
427
432
 
428
433
 
429
- # Splitting the source checklist across these two sections is what keeps a
430
- # person-owned gate ("validated in staging by <name>") out of the requirement
431
- # set. Both must exist even when empty, because a missing heading is
432
- # indistinguishable from "the reporter listed no gates" and the silent
433
- # version of that mistake is what puts an unmeetable requirement into
434
- # Requirement Coverage.
435
- SCOPE_SPLIT_SECTIONS = ("Acceptance Criteria", "External Gates")
434
+ # The brief pins the end state once; every later phase maps to it by id rather
435
+ # than restating a goal of its own. Splitting behavior from outcome is what lets
436
+ # a refactor say "nothing observable changes" without leaving the upper bound
437
+ # blank `Preserved Behavior` carries the bound that `Expected Behavior` can't.
438
+ _END_STATE_SECTIONS = (
439
+ ("Expected Behavior", "EB"),
440
+ ("Preserved Behavior", "PB"),
441
+ ("Expected Outcome", "EO"),
442
+ )
443
+ # The destination for a must-pass point nobody can hand an observation method.
444
+ # It is enforced here rather than in ALWAYS_REQUIRED_SECTIONS so the failure can
445
+ # still say WHY the section exists: a generic "required section missing" gives
446
+ # the reporter no way to tell a gate apart from an end state.
447
+ _GATE_SECTION = "External Gates"
448
+ _END_STATE_ID_RE = re.compile(r"^(?P<id>(?:EB|PB|EO)-\d{3})\b")
449
+ # Mirrors the `[—-]` tolerance in scripts/okstra_ctl/scope_provenance.py so a
450
+ # reporter who cannot type an em dash is not silently rejected.
451
+ _VERIFY_RE = re.compile(r"[—-]{1,2}\s*verify:\s*(?P<how>\S.*)$")
452
+ _SCAN_ONLY_PREFIXES = frozenset({"EB", "EO"})
453
+
454
+
455
+ def _has_bullet(body: str) -> bool:
456
+ return any(line.strip().startswith("- ") for line in body.splitlines())
457
+
458
+
459
+ def _declares_nothing(body: str) -> bool:
460
+ """True when the body is the contract's explicit empty marker and nothing else.
461
+
462
+ `_(none)_` is written bare in every other brief section — `is_placeholder`
463
+ accepts both the bare and the `- ` form, and the template tells the reporter
464
+ to "use _(none)_ if none". Demanding a dash here alone would reject the house
465
+ form, and the message that fires would tell the author to write the very
466
+ thing they wrote.
467
+ """
468
+ lines = [line.strip() for line in body.splitlines() if line.strip()]
469
+ return bool(lines) and all(is_placeholder(line) for line in lines)
436
470
 
437
471
 
438
- def check_scope_split_sections(text: str, errors: list[str]) -> None:
439
- """Both halves of the requirement/gate split must exist with a body.
472
+ def _check_gate_section(text: str, errors: list[str]) -> None:
473
+ if not has_section_heading(text, _GATE_SECTION):
474
+ errors.append(
475
+ f"required section '## {_GATE_SECTION}' is missing — it is where a "
476
+ "must-pass point that a person or live infrastructure owns goes. "
477
+ "Without it, an item okstra cannot satisfy has nowhere to land but "
478
+ "the end-state sections, and the plan is then forced to build a "
479
+ "stage for work no phase can do"
480
+ )
481
+ return
482
+ if not section_body(text, _GATE_SECTION).strip():
483
+ errors.append(
484
+ f"required section '## {_GATE_SECTION}' has an empty body "
485
+ "(use _(none)_ only for a deliberately-empty section)"
486
+ )
487
+
440
488
 
441
- `## Acceptance Criteria` holds must-pass points an okstra phase can satisfy
442
- by changing repository files; `## External Gates` holds the ones a person or
443
- live infrastructure owns (okstra-brief-gen SKILL.md §"Splitting a source
444
- checklist"). `_(none)_` stays valid — it is the contract's explicit
445
- placeholder for a deliberately-empty section.
446
- """
447
- for heading in SCOPE_SPLIT_SECTIONS:
448
- if not has_section_heading(text, heading):
489
+ def _check_end_state_items(
490
+ text: str, heading: str, prefix: str, seen: set[str], errors: list[str]
491
+ ) -> None:
492
+ for bullet in meaningful_bullets(text, heading):
493
+ match = _END_STATE_ID_RE.match(bullet)
494
+ if match is None or not match.group("id").startswith(f"{prefix}-"):
449
495
  errors.append(
450
- f"required section '## {heading}' is missing (a brief must split "
451
- "must-pass points into repo-satisfiable criteria and "
452
- "person/infrastructure-owned gates)"
496
+ f"'## {heading}' item {bullet!r} must start with a `{prefix}-NNN` id "
497
+ "(3 digits) later phases map to the brief by id, not by heading"
453
498
  )
454
499
  continue
455
- if not section_body(text, heading).strip():
500
+ item_id = match.group("id")
501
+ if item_id in seen:
502
+ errors.append(f"duplicate end-state id {item_id!r}")
503
+ seen.add(item_id)
504
+ if not _VERIFY_RE.search(bullet):
456
505
  errors.append(
457
- f"required section '## {heading}' has an empty body "
458
- "(use _(none)_ only for a deliberately-empty section)"
506
+ f"end-state item {item_id!r} has no observation method — append "
507
+ "`— verify: <command or observation point>`. An item nobody can "
508
+ "observe is not an end state. It has two destinations, both of "
509
+ f"which keep it in the brief: '## {_GATE_SECTION}' when a person "
510
+ "or live infrastructure owns it, or '## Open Questions' as a "
511
+ "`general:` row when the observation method is what is still "
512
+ "unknown. Deleting it is not one of them."
513
+ )
514
+
515
+
516
+ def _check_one_end_state_section(
517
+ text: str, heading: str, prefix: str, scope: str, seen: set[str], errors: list[str]
518
+ ) -> None:
519
+ """One end-state section: whether it may exist here, its body shape, its items.
520
+
521
+ A codebase-scan brief is authored before anything has been found, so it can
522
+ only pin what must NOT change. Declaring an expected behavior there would be
523
+ an invented requirement, which is why EB/EO are rejected rather than treated
524
+ as optional — and why `_(none)_` is rejected for the one section that
525
+ survives there: it is the whole upper bound of that variant.
526
+ """
527
+ if scope == "codebase" and prefix in _SCAN_ONLY_PREFIXES:
528
+ if has_section_heading(text, heading):
529
+ errors.append(
530
+ f"section '## {heading}' is not allowed in a codebase-scan brief "
531
+ "(the scan has not run yet, so its outcome cannot be declared) — "
532
+ "use '## Preserved Behavior' for what must not change"
533
+ )
534
+ return
535
+ if not has_section_heading(text, heading):
536
+ errors.append(
537
+ f"required section '## {heading}' is missing (the brief pins the "
538
+ "end state that every later phase maps to)"
539
+ )
540
+ return
541
+ body = section_body(text, heading)
542
+ if not body.strip():
543
+ errors.append(
544
+ f"required section '## {heading}' has an empty body "
545
+ "(use _(none)_ only for a deliberately-empty section)"
546
+ )
547
+ return
548
+ if _declares_nothing(body):
549
+ # In a scan this is the only section left, so an empty marker here is
550
+ # not "deliberately empty" — it is a brief with no upper bound at all,
551
+ # which is what improvement-discovery reads as its bound.
552
+ if scope == "codebase":
553
+ errors.append(
554
+ f"'## {heading}' is _(none)_ in a codebase-scan brief. It is the "
555
+ "only end-state section this variant carries, and "
556
+ "improvement-discovery treats it as the bound on what a "
557
+ "candidate may change — an empty one bounds nothing. State at "
558
+ "least one behaviour no candidate from this scan may break"
459
559
  )
560
+ return
561
+ # Only `- ` lines are read as items, so prose here is silently dropped
562
+ # rather than rejected — the reporter believes they pinned an end state
563
+ # and every downstream phase sees a section that declared nothing.
564
+ if not _has_bullet(body):
565
+ errors.append(
566
+ f"'## {heading}' has a body but no `- ` item — prose in this "
567
+ "section is dropped, not read. Every end state is a "
568
+ f"`- {prefix}-NNN <condition> — verify: <how>` bullet; write "
569
+ "_(none)_ on its own line if the section is deliberately empty"
570
+ )
571
+ return
572
+ _check_end_state_items(text, heading, prefix, seen, errors)
573
+
574
+
575
+ def _check_end_state_declared(text: str, errors: list[str]) -> None:
576
+ """A reporter-input brief declares at least one observable end state."""
577
+ if meaningful_bullets(text, "Expected Behavior") or meaningful_bullets(
578
+ text, "Expected Outcome"
579
+ ):
580
+ return
581
+ errors.append(
582
+ "'## Expected Behavior' and '## Expected Outcome' are both empty — a brief "
583
+ "must declare at least one observable end state. A pure bugfix may leave "
584
+ "Expected Outcome as _(none)_ and a pure refactor may leave Expected "
585
+ "Behavior as _(none)_, but not both. If every must-pass point you have is "
586
+ f"owned by a person ('## {_GATE_SECTION}') or still lacks an observation "
587
+ "method ('## Open Questions'), this brief is not ready to start a run — "
588
+ "answer the open question first rather than emptying the end state"
589
+ )
590
+
591
+
592
+ def check_end_state_sections(text: str, scope: str, errors: list[str]) -> None:
593
+ """The brief's end state: expected behavior, preserved behavior, expected outcome.
594
+
595
+ `## External Gates` is checked here too, in every variant: it is where an
596
+ item with no observation method goes, so the admissible set and its overflow
597
+ have to be required by the same check or the overflow silently disappears.
598
+ """
599
+ _check_gate_section(text, errors)
600
+ seen: set[str] = set()
601
+ for heading, prefix in _END_STATE_SECTIONS:
602
+ _check_one_end_state_section(text, heading, prefix, scope, seen, errors)
603
+ if scope != "codebase":
604
+ _check_end_state_declared(text, errors)
460
605
 
461
606
 
462
607
  # The variant-independent rows of the skill's "Required sections by variant"
463
- # table (okstra-brief-gen SKILL.md). Desired Outcome and the Acceptance
464
- # Criteria / External Gates split have dedicated checks with richer messages;
608
+ # table (okstra-brief-gen SKILL.md). Desired Outcome and the end-state
609
+ # sections / External Gates have dedicated checks with richer messages;
465
610
  # Scan Scope / Priority Lenses are codebase-only (check_codebase_scope);
466
611
  # Source Material / Problem / Symptom are omitted by the codebase variant, so
467
612
  # they are enforced only off it.
@@ -563,7 +708,7 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
563
708
  check_related_task_graph(text, errors)
564
709
 
565
710
  check_requirement_section(text, errors)
566
- check_scope_split_sections(text, errors)
711
+ check_end_state_sections(text, scope, errors)
567
712
  check_variant_required_sections(text, scope, errors)
568
713
 
569
714
  # 2. brief-id matches filename stem