devrites 6.0.2 → 6.0.3

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 (146) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/docs/adr/0034-workflow-artifact-byte-corrections-get-code-review.md +60 -0
  4. package/docs/adr/README.md +2 -1
  5. package/docs/engine/commands.md +6 -6
  6. package/engine/help.go +12 -7
  7. package/engine/help_test.go +1 -0
  8. package/engine/internal/gate/readiness_binding.go +19 -0
  9. package/engine/internal/lib/cli_observe.go +16 -0
  10. package/engine/internal/lib/context.go +191 -19
  11. package/engine/internal/lib/context_next_test.go +186 -0
  12. package/engine/internal/lib/dispatch.go +146 -11
  13. package/engine/internal/lib/drift.go +10 -2
  14. package/engine/internal/lib/drift_test.go +29 -0
  15. package/engine/internal/lib/slicecheck.go +19 -0
  16. package/engine/internal/lib/slicecheck_test.go +41 -0
  17. package/engine/internal/lib/windows.go +109 -13
  18. package/engine/internal/lib/windows_test.go +85 -0
  19. package/engine/main.go +2 -2
  20. package/pack/.claude/agents/devrites-slice-wright.md +3 -1
  21. package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  22. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  23. package/pack/.claude/skills/devrites-lib/reference/standards/README.md +1 -1
  24. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  25. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
  26. package/pack/.claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
  27. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +1 -0
  28. package/pack/.claude/skills/devrites-lib/reference/standards/windows.md +11 -3
  29. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  30. package/pack/.claude/skills/rite-build/SKILL.md +1 -1
  31. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  32. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -3
  33. package/pack/.claude/skills/rite-build/reference/phase-contract.md +5 -3
  34. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
  35. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +23 -9
  36. package/pack/.claude/skills/rite-define/SKILL.md +2 -1
  37. package/pack/.claude/skills/rite-plan/SKILL.md +2 -1
  38. package/pack/.claude/skills/rite-pressure-test/SKILL.md +6 -3
  39. package/pack/.claude/skills/rite-vet/SKILL.md +9 -1
  40. package/pack/.claude/skills/rite-vet/reference/review-axes.md +6 -2
  41. package/pack/generated/claude/agents/devrites-slice-wright.md +3 -1
  42. package/pack/generated/claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  43. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  44. package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +1 -1
  45. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  46. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
  47. package/pack/generated/claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
  48. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +1 -0
  49. package/pack/generated/claude/skills/devrites-lib/reference/standards/windows.md +11 -3
  50. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  51. package/pack/generated/claude/skills/rite-build/SKILL.md +1 -1
  52. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  53. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -3
  54. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +5 -3
  55. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
  56. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +23 -9
  57. package/pack/generated/claude/skills/rite-define/SKILL.md +2 -1
  58. package/pack/generated/claude/skills/rite-plan/SKILL.md +2 -1
  59. package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +6 -3
  60. package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -1
  61. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +6 -2
  62. package/pack/generated/codex/agents/devrites-slice-wright.toml +3 -1
  63. package/pack/generated/codex/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  64. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  65. package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +1 -1
  66. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  67. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +3 -0
  68. package/pack/generated/codex/skills/devrites-lib/reference/standards/glossary.md +1 -1
  69. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +1 -0
  70. package/pack/generated/codex/skills/devrites-lib/reference/standards/windows.md +11 -3
  71. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  72. package/pack/generated/codex/skills/rite-build/SKILL.md +1 -1
  73. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
  74. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -3
  75. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +5 -3
  76. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -1
  77. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +23 -9
  78. package/pack/generated/codex/skills/rite-define/SKILL.md +2 -1
  79. package/pack/generated/codex/skills/rite-plan/SKILL.md +2 -1
  80. package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -3
  81. package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -1
  82. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +6 -2
  83. package/pack/generated/devin/agents/devrites-slice-wright.md +3 -1
  84. package/pack/generated/devin/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  85. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  86. package/pack/generated/devin/skills/devrites-lib/reference/standards/README.md +1 -1
  87. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  88. package/pack/generated/devin/skills/devrites-lib/reference/standards/code-review.md +3 -0
  89. package/pack/generated/devin/skills/devrites-lib/reference/standards/glossary.md +1 -1
  90. package/pack/generated/devin/skills/devrites-lib/reference/standards/security.md +1 -0
  91. package/pack/generated/devin/skills/devrites-lib/reference/standards/windows.md +11 -3
  92. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  93. package/pack/generated/devin/skills/rite-build/SKILL.md +1 -1
  94. package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
  95. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -3
  96. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +5 -3
  97. package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -1
  98. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +23 -9
  99. package/pack/generated/devin/skills/rite-define/SKILL.md +2 -1
  100. package/pack/generated/devin/skills/rite-plan/SKILL.md +2 -1
  101. package/pack/generated/devin/skills/rite-pressure-test/SKILL.md +6 -3
  102. package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -1
  103. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +6 -2
  104. package/pack/generated/omp/agents/devrites-slice-wright.md +3 -1
  105. package/pack/generated/omp/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  106. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  107. package/pack/generated/omp/skills/devrites-lib/reference/standards/README.md +1 -1
  108. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  109. package/pack/generated/omp/skills/devrites-lib/reference/standards/code-review.md +3 -0
  110. package/pack/generated/omp/skills/devrites-lib/reference/standards/glossary.md +1 -1
  111. package/pack/generated/omp/skills/devrites-lib/reference/standards/security.md +1 -0
  112. package/pack/generated/omp/skills/devrites-lib/reference/standards/windows.md +11 -3
  113. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  114. package/pack/generated/omp/skills/rite-build/SKILL.md +1 -1
  115. package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
  116. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -3
  117. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +5 -3
  118. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -1
  119. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +23 -9
  120. package/pack/generated/omp/skills/rite-define/SKILL.md +2 -1
  121. package/pack/generated/omp/skills/rite-plan/SKILL.md +2 -1
  122. package/pack/generated/omp/skills/rite-pressure-test/SKILL.md +6 -3
  123. package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -1
  124. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +6 -2
  125. package/pack/generated/pi/agents/devrites-slice-wright.md +3 -1
  126. package/pack/generated/pi/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  127. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  128. package/pack/generated/pi/skills/devrites-lib/reference/standards/README.md +1 -1
  129. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  130. package/pack/generated/pi/skills/devrites-lib/reference/standards/code-review.md +3 -0
  131. package/pack/generated/pi/skills/devrites-lib/reference/standards/glossary.md +1 -1
  132. package/pack/generated/pi/skills/devrites-lib/reference/standards/security.md +1 -0
  133. package/pack/generated/pi/skills/devrites-lib/reference/standards/windows.md +11 -3
  134. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  135. package/pack/generated/pi/skills/rite-build/SKILL.md +1 -1
  136. package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
  137. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -3
  138. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +5 -3
  139. package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -1
  140. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +23 -9
  141. package/pack/generated/pi/skills/rite-define/SKILL.md +2 -1
  142. package/pack/generated/pi/skills/rite-plan/SKILL.md +2 -1
  143. package/pack/generated/pi/skills/rite-pressure-test/SKILL.md +6 -3
  144. package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -1
  145. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +6 -2
  146. package/package.json +1 -1
@@ -672,3 +672,189 @@ func TestContextWorkspaceIncludeMarkerStaysLiteral(t *testing.T) {
672
672
  t.Fatalf("workspace include marker not kept literal:\n%s", text)
673
673
  }
674
674
  }
675
+
676
+ func TestContextSliceScopesWorkspaceAndRole(t *testing.T) {
677
+ root, featureDir := newWorkspace(t, "build")
678
+ skills := newSkillsRoot(t)
679
+ writeFile(t, filepath.Join(skills, "rite-build", "reference", "root-procedure.md"), "# root procedure body\n")
680
+ writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\",\"rite-build/reference/root-procedure.md\"],\"rootOnly\":[\"rite-build/reference/root-procedure.md\"],\"workspace\":[\"tasks.md\",\"decisions.md\",\"spec.md\"]} -->\n# build\n")
681
+ writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: first body\n\n## SLICE-002 Second\nGoal: second body\nSatisfies: AC-001\nDecisions: DEC-010\ndepends_on: [SLICE-001]\n")
682
+ var decisions strings.Builder
683
+ decisions.WriteString("# Decisions\n")
684
+ for i := 0; i < 250; i++ {
685
+ decisions.WriteString("filler decision row\n")
686
+ }
687
+ decisions.WriteString("## DEC-010 Use the store\nDEC-0100 is a different id\n")
688
+ writeFile(t, filepath.Join(featureDir, "decisions.md"), decisions.String())
689
+ writeFile(t, filepath.Join(featureDir, "spec.md"), "# spec\nAC-001 small spec body\n")
690
+
691
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
692
+ args := []string{"feat", "--phase", "build", "--role", "slice-wright", "--slice", "SLICE-002", "--skills-root", skills}
693
+ if code := RunContext(root, args, stdout, stderr); code != 0 {
694
+ t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
695
+ }
696
+ if !strings.Contains(stdout.String(), "scope: slice SLICE-002 (1 whole, 2 by-reference)") {
697
+ t.Fatalf("scope line missing:\n%s", stdout)
698
+ }
699
+ raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build-slice-wright.SLICE-002.bundle.md"))
700
+ if err != nil {
701
+ t.Fatalf("bundle: %v", err)
702
+ }
703
+ text := string(raw)
704
+ for _, want := range []string{
705
+ "Goal: second body",
706
+ "workspace/decisions.md (by reference)",
707
+ "/work/feat/decisions.md sha256: ",
708
+ "- L252 ## DEC-010 Use the store",
709
+ "- DEC-010: L252\n",
710
+ "- SLICE-001: L3 L10\n",
711
+ "- AC-001: L8\n",
712
+ "small spec body",
713
+ "===== root-only (not inlined for role slice-wright) =====\nrite-build/reference/root-procedure.md",
714
+ } {
715
+ if !strings.Contains(text, want) {
716
+ t.Fatalf("bundle missing %q:\n%s", want, text)
717
+ }
718
+ }
719
+ for _, absent := range []string{"Goal: first body", "filler decision row", "root procedure body"} {
720
+ if strings.Contains(text, absent) {
721
+ t.Fatalf("bundle inlined %q:\n%s", absent, text)
722
+ }
723
+ }
724
+ if strings.Index(text, "# wright") > strings.Index(text, "# core") {
725
+ t.Fatalf("role contract must lead the bundle:\n%s", text)
726
+ }
727
+
728
+ // Without --role the root still gets its procedure inline.
729
+ stdout.Reset()
730
+ if code := RunContext(root, []string{"feat", "--phase", "build", "--skills-root", skills}, stdout, stderr); code != 0 {
731
+ t.Fatalf("phase bundle code=%d\n%s", code, stderr)
732
+ }
733
+ phase, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.bundle.md"))
734
+ if err != nil || !strings.Contains(string(phase), "root procedure body") || !strings.Contains(string(phase), "Goal: first body") {
735
+ t.Fatalf("phase bundle must stay whole: err=%v\n%s", err, phase)
736
+ }
737
+ }
738
+
739
+ func TestContextSliceRejectsBadOrUnknownID(t *testing.T) {
740
+ root, featureDir := newWorkspace(t, "build")
741
+ skills := newSkillsRoot(t)
742
+ writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: x\n")
743
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
744
+ if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "slice-1", "--skills-root", skills}, stdout, stderr); code != 2 {
745
+ t.Fatalf("malformed id: code=%d want 2\n%s", code, stderr)
746
+ }
747
+ if code := RunContext(root, []string{"--skill", "rite-build", "--slice", "SLICE-001", "--out", "x.md", "--skills-root", skills}, stdout, stderr); code != 2 {
748
+ t.Fatalf("slice without slug: code=%d want 2\n%s", code, stderr)
749
+ }
750
+ stdout.Reset()
751
+ if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "SLICE-009", "--skills-root", skills}, stdout, stderr); code != 3 || !strings.Contains(stdout.String(), "BLOCKED: SLICE-009 not found") {
752
+ t.Fatalf("unknown slice: code=%d\n%s", code, stdout)
753
+ }
754
+ }
755
+
756
+ func TestDispatchWrightRoundCap(t *testing.T) {
757
+ root, featureDir := newWorkspace(t, "build")
758
+ run := func(args ...string) (int, string) {
759
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
760
+ code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
761
+ return code, stdout.String() + stderr.String()
762
+ }
763
+ open := func(wave string, extra ...string) (int, string) {
764
+ return run(append([]string{"open", "--phase", "build", "--wave", wave, "--role", "slice-wright", "--slice", "SLICE-001"}, extra...)...)
765
+ }
766
+ if code, out := run("open", "--phase", "build", "--wave", "w0", "--role", "slice-wright"); code != 2 || !strings.Contains(out, "needs --slice") {
767
+ t.Fatalf("missing --slice: code=%d\n%s", code, out)
768
+ }
769
+ if code, out := run("open", "--phase", "vet", "--wave", "v1", "--role", "plan-reviewer"); code != 0 {
770
+ t.Fatalf("non-build wave must not need --slice: code=%d\n%s", code, out)
771
+ }
772
+ for i := 1; i <= wrightRoundsPerReview; i++ {
773
+ code, out := open("r" + string(rune('0'+i)))
774
+ if code != 0 || !strings.Contains(out, "SLICE-001 wright round "+string(rune('0'+i))) {
775
+ t.Fatalf("round %d: code=%d\n%s", i, code, out)
776
+ }
777
+ }
778
+ if code, out := run("abandon", "--wave", "r5", "--reason", "orphaned"); !strings.Contains(out, "wave r5 abandoned") {
779
+ t.Fatalf("abandon: code=%d\n%s", code, out)
780
+ }
781
+ if code, out := open("r5b"); code != 0 || !strings.Contains(out, "wright round 5") {
782
+ t.Fatalf("abandoned wave must not count: code=%d\n%s", code, out)
783
+ }
784
+ if code, out := open("r6"); code != 3 || !strings.Contains(out, "BLOCKED: SLICE-001 reached 5 wright rounds") {
785
+ t.Fatalf("round 6 must block: code=%d\n%s", code, out)
786
+ }
787
+ writeFile(t, filepath.Join(featureDir, "drift.md"), "# Drift\n\n## D-010 — SLICE-002 unrelated\n\n## D-011 — SLICE-001 reslice review: no smaller AC-preserving split\n")
788
+ if code, out := open("r6", "--reslice-review", "D-099"); code != 3 {
789
+ t.Fatalf("unknown drift id: code=%d\n%s", code, out)
790
+ }
791
+ if code, out := open("r6", "--reslice-review", "D-010"); code != 3 || !strings.Contains(out, "naming SLICE-001") {
792
+ t.Fatalf("drift heading for another slice: code=%d\n%s", code, out)
793
+ }
794
+ if code, out := open("r6", "--reslice-review", "D-011"); code != 0 || !strings.Contains(out, "wright round 6") {
795
+ t.Fatalf("valid review must admit: code=%d\n%s", code, out)
796
+ }
797
+ for i := 7; i <= 2*wrightRoundsPerReview; i++ {
798
+ if code, out := open("r" + string(rune('a'+i))); code != 0 {
799
+ t.Fatalf("round %d inside the reviewed window: code=%d\n%s", i, code, out)
800
+ }
801
+ }
802
+ if code, out := open("r11", "--reslice-review", "D-011"); code != 3 || !strings.Contains(out, "already admitted") {
803
+ t.Fatalf("reused review: code=%d\n%s", code, out)
804
+ }
805
+ if code, out := run("open", "--phase", "build", "--wave", "s2", "--role", "slice-wright", "--slice", "SLICE-002", "--reslice-review", "D-010"); code != 2 {
806
+ t.Fatalf("review before the cap must be refused: code=%d\n%s", code, out)
807
+ }
808
+ }
809
+
810
+ func TestDispatchVetRoundsCountSeparately(t *testing.T) {
811
+ root, featureDir := newWorkspace(t, "build")
812
+ run := func(args ...string) (int, string) {
813
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
814
+ code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
815
+ return code, stdout.String() + stderr.String()
816
+ }
817
+ for i := 0; i < 7; i++ {
818
+ if code, out := run("open", "--phase", "vet", "--wave", "full"+string(rune('a'+i)), "--role", "plan-reviewer"); code != 0 {
819
+ t.Fatalf("full Vet without --slice stays uncapped: code=%d\n%s", code, out)
820
+ }
821
+ }
822
+ if code, out := run("open", "--phase", "build", "--wave", "w1", "--role", "slice-wright", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "wright round 1") {
823
+ t.Fatalf("wright round must not see vet waves: code=%d\n%s", code, out)
824
+ }
825
+ for i := 1; i <= wrightRoundsPerReview; i++ {
826
+ if code, out := run("open", "--phase", "vet", "--wave", "v"+string(rune('0'+i)), "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "vet round "+string(rune('0'+i))) {
827
+ t.Fatalf("vet round %d: code=%d\n%s", i, code, out)
828
+ }
829
+ }
830
+ if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 3 || !strings.Contains(out, "reached 5 vet rounds") {
831
+ t.Fatalf("vet round 6 must block: code=%d\n%s", code, out)
832
+ }
833
+ writeFile(t, filepath.Join(featureDir, "drift.md"), "## D-001 — SLICE-001 reslice review: proof seam already minimal\n")
834
+ if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001", "--reslice-review", "D-001"); code != 0 {
835
+ t.Fatalf("reviewed vet round 6: code=%d\n%s", code, out)
836
+ }
837
+ if code, out := run("open", "--phase", "plan", "--wave", "p1", "--role", "plan-drafter", "--slice", "SLICE-001"); code != 2 {
838
+ t.Fatalf("--slice on a plan wave: code=%d\n%s", code, out)
839
+ }
840
+ }
841
+
842
+ func TestContextRepeatedSliceUnion(t *testing.T) {
843
+ root, featureDir := newWorkspace(t, "build")
844
+ skills := newSkillsRoot(t)
845
+ writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\"],\"workspace\":[\"tasks.md\"]} -->\n# build\n")
846
+ writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 A\nGoal: alpha body\n\n## SLICE-002 B\nGoal: beta body\n\n## SLICE-003 C\nGoal: gamma body\n")
847
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
848
+ args := []string{"feat", "--phase", "build", "--slice", "SLICE-001", "--slice", "SLICE-003", "--skills-root", skills}
849
+ if code := RunContext(root, args, stdout, stderr); code != 0 {
850
+ t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
851
+ }
852
+ raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.SLICE-001+SLICE-003.bundle.md"))
853
+ if err != nil {
854
+ t.Fatal(err)
855
+ }
856
+ text := string(raw)
857
+ if !strings.Contains(text, "alpha body") || !strings.Contains(text, "gamma body") || strings.Contains(text, "beta body") {
858
+ t.Fatalf("union of slice blocks wrong:\n%s", text)
859
+ }
860
+ }
@@ -6,6 +6,8 @@ import (
6
6
  "io"
7
7
  "os"
8
8
  "path/filepath"
9
+ "regexp"
10
+ "slices"
9
11
  "sort"
10
12
  "strings"
11
13
  "time"
@@ -19,24 +21,38 @@ import (
19
21
  // fan-out cannot silently degrade to serial (open -> every role started ->
20
22
  // seal -> returns -> complete). start/return also auto-record metrics events.
21
23
  //
22
- // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s]
24
+ // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>]
23
25
 
24
- const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon> [--phase p] [--wave w] [--role r] [--handle h] [--reason s]
26
+ const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>]
25
27
 
26
28
  Wave barrier for parallel agent dispatch: seal fails until every declared
27
29
  role has a distinct handle; return fails before seal; status exits nonzero
28
30
  while a wave is open, sealed, or abandoned.
31
+
32
+ A build wave with a wright role must name its slice(s) with --slice; a vet
33
+ wave may (slice-bound recheck). Every 5 rounds of one kind on one slice, the
34
+ next open is blocked until --reslice-review names a drift.md
35
+ "## <DRIFT-ID>" heading for that slice.
29
36
  `
30
37
 
38
+ // wrightRoundsPerReview is how many build wright rounds (or slice-bound Vet
39
+ // rechecks) one slice may take before (and after each) reslice review.
40
+ // Recovery itself stays unbounded; the cap only forces the split question
41
+ // onto the record.
42
+ const wrightRoundsPerReview = 5
43
+
31
44
  type dispatchFile struct {
32
45
  Waves map[string]*dispatchWave `json:"waves"`
33
46
  }
34
47
 
35
48
  type dispatchWave struct {
36
- Phase string `json:"phase"`
37
- State string `json:"state"` // open, sealed, abandoned, complete
38
- Reason string `json:"reason,omitempty"`
39
- Roles map[string]*dispatchRole `json:"roles"`
49
+ Phase string `json:"phase"`
50
+ State string `json:"state"` // open, sealed, abandoned, complete
51
+ Reason string `json:"reason,omitempty"`
52
+ Roles map[string]*dispatchRole `json:"roles"`
53
+ Slices []string `json:"slices,omitempty"`
54
+ ResliceReview string `json:"reslice_review,omitempty"`
55
+ ResliceReviewed []string `json:"reslice_reviewed,omitempty"`
40
56
  }
41
57
 
42
58
  type dispatchRole struct {
@@ -89,8 +105,8 @@ func nowStamp() string { return time.Now().UTC().Format(time.RFC3339) }
89
105
 
90
106
  // RunDispatch implements the dispatch wave state machine.
91
107
  func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
92
- var slug, sub, phase, wave, role, handle, reason string
93
- var roles []string
108
+ var slug, sub, phase, wave, role, handle, reason, review string
109
+ var roles, sliceIDs []string
94
110
  var positional []string
95
111
  for i := 0; i < len(args); i++ {
96
112
  value := argAt(args, i+1)
@@ -106,6 +122,10 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
106
122
  handle = value
107
123
  case "--reason":
108
124
  reason = value
125
+ case "--slice":
126
+ sliceIDs = append(sliceIDs, value)
127
+ case "--reslice-review":
128
+ review = value
109
129
  default:
110
130
  if strings.HasPrefix(args[i], "-") {
111
131
  fmt.Fprintf(stderr, "dispatch: unknown flag %q\n", args[i])
@@ -129,7 +149,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
129
149
  // an update and strand the wave short of complete.
130
150
  code := 0
131
151
  if err := state.WithFeatureLock(root, slug, func() error {
132
- code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, stdout, stderr)
152
+ code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, stdout, stderr)
133
153
  return nil
134
154
  }); err != nil {
135
155
  fmt.Fprintf(stderr, "dispatch: %v\n", err)
@@ -138,7 +158,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
138
158
  return code
139
159
  }
140
160
 
141
- func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles []string, stdout, stderr io.Writer) int {
161
+ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles, sliceIDs []string, review string, stdout, stderr io.Writer) int {
142
162
  f, path, err := loadDispatch(root, slug)
143
163
  if err != nil {
144
164
  fmt.Fprintf(stderr, "dispatch: %v\n", err)
@@ -166,18 +186,25 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
166
186
  if w := getWave(); w != nil && w.State != "abandoned" {
167
187
  return fail("wave %q already exists (state %s)", wave, w.State)
168
188
  }
169
- w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}}
189
+ w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}, Slices: sliceIDs}
170
190
  for _, r := range roles {
171
191
  if w.Roles[r] != nil {
172
192
  return fail("duplicate role %q in wave", r)
173
193
  }
174
194
  w.Roles[r] = &dispatchRole{}
175
195
  }
196
+ rounds, code := admitWrightRounds(root, slug, f, w, review, stderr)
197
+ if code != 0 {
198
+ return code
199
+ }
176
200
  f.Waves[wave] = w
177
201
  if err := saveDispatch(path, f); err != nil {
178
202
  return fail("%v", err)
179
203
  }
180
204
  fmt.Fprintf(stdout, "dispatch: wave %s open, %d roles declared\n", wave, len(roles))
205
+ for _, line := range rounds {
206
+ fmt.Fprintln(stdout, line)
207
+ }
181
208
  return 0
182
209
 
183
210
  case "start":
@@ -322,6 +349,114 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
322
349
  return 2
323
350
  }
324
351
 
352
+ // roundKind names the capped loop a wave belongs to: build wright rounds or
353
+ // slice-bound Vet rechecks. Other waves are uncapped.
354
+ func roundKind(w *dispatchWave) string {
355
+ switch {
356
+ case isBuildWrightWave(w):
357
+ return "wright"
358
+ case strings.EqualFold(w.Phase, "vet") && len(w.Slices) > 0:
359
+ return "vet"
360
+ }
361
+ return ""
362
+ }
363
+
364
+ func isBuildWrightWave(w *dispatchWave) bool {
365
+ if w.Phase != "build" {
366
+ return false
367
+ }
368
+ for r := range w.Roles {
369
+ if strings.Contains(r, "wright") {
370
+ return true
371
+ }
372
+ }
373
+ return false
374
+ }
375
+
376
+ // admitWrightRounds enforces the per-slice round cap for a new build wright
377
+ // or slice-bound Vet wave and returns the round lines to print. A slice may
378
+ // take wrightRoundsPerReview rounds of each kind per reslice review (the first
379
+ // window is free); beyond that the open needs --reslice-review naming an
380
+ // unused drift.md heading for every slice over the cap.
381
+ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, review string, stderr io.Writer) ([]string, int) {
382
+ kind := roundKind(w)
383
+ if kind == "" {
384
+ if review != "" || (len(w.Slices) > 0 && !strings.EqualFold(w.Phase, "vet")) {
385
+ fmt.Fprintln(stderr, "dispatch: --slice/--reslice-review apply only to build wright waves and slice-bound vet waves")
386
+ return nil, 2
387
+ }
388
+ return nil, 0
389
+ }
390
+ if len(w.Slices) == 0 {
391
+ fmt.Fprintln(stderr, "dispatch: a build wright wave needs --slice SLICE-### for each slice it builds")
392
+ return nil, 2
393
+ }
394
+ for _, id := range w.Slices {
395
+ if !state.ValidSliceID(id) {
396
+ fmt.Fprintf(stderr, "dispatch: %q is not a SLICE-### id\n", id)
397
+ return nil, 2
398
+ }
399
+ }
400
+ var lines, overCap []string
401
+ for _, id := range w.Slices {
402
+ prior, reviews := 0, 0
403
+ for _, other := range f.Waves {
404
+ if other.State == "abandoned" || roundKind(other) != kind {
405
+ continue
406
+ }
407
+ if slices.Contains(other.Slices, id) {
408
+ prior++
409
+ }
410
+ if slices.Contains(other.ResliceReviewed, id) {
411
+ reviews++
412
+ }
413
+ }
414
+ round := prior + 1
415
+ if round > wrightRoundsPerReview*(1+reviews) {
416
+ overCap = append(overCap, id)
417
+ }
418
+ lines = append(lines, fmt.Sprintf("dispatch: %s %s round %d (reslice review every %d)", id, kind, round, wrightRoundsPerReview))
419
+ }
420
+ if len(overCap) == 0 {
421
+ if review != "" {
422
+ fmt.Fprintf(stderr, "dispatch: --reslice-review given but no slice has reached its %d-round cap\n", wrightRoundsPerReview)
423
+ return nil, 2
424
+ }
425
+ return lines, 0
426
+ }
427
+ if review == "" {
428
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: %s reached %d %s rounds; run a reslice review (acceptance-preserving split or simpler proof seam, or a drift.md entry recording why neither applies) and reopen with --reslice-review <DRIFT-ID>\n",
429
+ strings.Join(overCap, ","), wrightRoundsPerReview, kind)
430
+ return nil, 3
431
+ }
432
+ for _, other := range f.Waves {
433
+ if other.State != "abandoned" && other.ResliceReview == review && roundKind(other) == kind {
434
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: reslice review %s already admitted another round window\n", review)
435
+ return nil, 3
436
+ }
437
+ }
438
+ drift, err := readWorkspaceArtifact(root, slug, "drift.md")
439
+ if err != nil {
440
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: reslice review %s: %v\n", review, err)
441
+ return nil, 3
442
+ }
443
+ heading := driftHeading(string(drift), review)
444
+ for _, id := range overCap {
445
+ if !regexp.MustCompile(`\b` + regexp.QuoteMeta(id) + `\b`).MatchString(heading) {
446
+ fmt.Fprintf(stderr, "dispatch: BLOCKED: drift.md has no \"## %s\" heading naming %s\n", review, id)
447
+ return nil, 3
448
+ }
449
+ }
450
+ w.ResliceReview, w.ResliceReviewed = review, overCap
451
+ return lines, 0
452
+ }
453
+
454
+ // driftHeading returns the drift.md level-2 heading line for id, or "".
455
+ func driftHeading(drift, id string) string {
456
+ re := regexp.MustCompile(`(?m)^## ` + regexp.QuoteMeta(id) + `\b.*$`)
457
+ return re.FindString(drift)
458
+ }
459
+
325
460
  func waveState(w *dispatchWave) string {
326
461
  if w == nil {
327
462
  return "absent"
@@ -48,7 +48,7 @@ type driftBaseline struct {
48
48
  // driftDiff is one input whose state differs from the baseline.
49
49
  type driftDiff struct {
50
50
  Path string `json:"path"`
51
- Kind string `json:"kind"` // changed | missing | added
51
+ Kind string `json:"kind"` // changed | missing | added | reviewed-identity-only
52
52
  }
53
53
 
54
54
  // RunCheckDrift implements `devrites-engine check drift`.
@@ -130,6 +130,10 @@ func compareDrift(workspace, baselinePath string, digests []gate.ReadinessInputD
130
130
  if baseline.Binding != binding && len(diffs) == 0 {
131
131
  fmt.Fprintln(stdout, "DRIFT: aggregate binding changed without a recorded input diff; re-record the baseline")
132
132
  }
133
+ if len(diffs) == 1 && diffs[0].Kind == "reviewed-identity-only" {
134
+ fmt.Fprintln(stdout, "check drift: reviewed-identity-only — only the Workflow Artifact reviewed identity moved; /rite-vet may use its bytes-only recheck")
135
+ return 0
136
+ }
133
137
  fmt.Fprintf(stdout, "check drift: %d input(s) differ — a change is legal only as a dated delta through the owning rite; otherwise restore the input or rerun /rite-vet and --record\n", len(diffs))
134
138
  return 0
135
139
  }
@@ -189,7 +193,11 @@ func diffInputs(base, current []gate.ReadinessInputDigest) []driftDiff {
189
193
  }
190
194
  diffs = append(diffs, driftDiff{Path: b.Path, Kind: kind})
191
195
  case b.Present && b.SHA256 != c.SHA256:
192
- diffs = append(diffs, driftDiff{Path: b.Path, Kind: "changed"})
196
+ kind := "changed"
197
+ if b.Normalized != "" && b.Normalized == c.Normalized {
198
+ kind = "reviewed-identity-only"
199
+ }
200
+ diffs = append(diffs, driftDiff{Path: b.Path, Kind: kind})
193
201
  }
194
202
  }
195
203
  sort.Slice(diffs, func(i, j int) bool { return diffs[i].Path < diffs[j].Path })
@@ -104,3 +104,32 @@ func TestDriftCorruptBaselineBlocks(t *testing.T) {
104
104
  t.Fatalf("code=%d out=%s", code, out)
105
105
  }
106
106
  }
107
+
108
+ func TestDriftClassifiesReviewedIdentityOnlyChange(t *testing.T) {
109
+ root, featureDir := driftWorkspace(t)
110
+ if code, out := checkDrift(t, root, "feat", "--record"); code != 0 {
111
+ t.Fatalf("record code=%d out=%s", code, out)
112
+ }
113
+ plan := filepath.Join(featureDir, "test-plan.md")
114
+ line := "Workflow Artifact reviewed identity: " + strings.Repeat("ab", 32) + "\n"
115
+ writeFile(t, plan, "# test-plan.md\n\ncontent\n"+line)
116
+ code, out := checkDrift(t, root, "feat")
117
+ if code != 0 || !strings.Contains(out, "test-plan.md reviewed-identity-only") || !strings.Contains(out, "bytes-only recheck") {
118
+ t.Fatalf("identity-only change not classified: code=%d\n%s", code, out)
119
+ }
120
+ // Any other edit, including a malformed identity line, is ordinary drift.
121
+ for _, body := range []string{
122
+ "# test-plan.md\n\nchanged\n" + line,
123
+ "# test-plan.md\n\ncontent\nWorkflow Artifact reviewed identity: not-a-digest\n",
124
+ } {
125
+ writeFile(t, plan, body)
126
+ if _, out := checkDrift(t, root, "feat"); !strings.Contains(out, "test-plan.md changed") || strings.Contains(out, "bytes-only") {
127
+ t.Fatalf("non-identity edit hidden:\n%s", out)
128
+ }
129
+ }
130
+ writeFile(t, plan, "# test-plan.md\n\ncontent\n"+line)
131
+ writeFile(t, filepath.Join(featureDir, "plan.md"), "# plan.md\n\nrewritten\n")
132
+ if _, out := checkDrift(t, root, "feat"); strings.Contains(out, "bytes-only") {
133
+ t.Fatalf("a second changed input must void the bytes-only route:\n%s", out)
134
+ }
135
+ }
@@ -29,6 +29,22 @@ var (
29
29
  windowsDrivePath = regexp.MustCompile(`^[A-Za-z]:[\\/]`)
30
30
  )
31
31
 
32
+ // sliceFileAdvisory is the Writer allowlist size at which a slice is flagged
33
+ // for an acceptance-preserving split. Advisory only: it never changes an exit
34
+ // code. Measured on real plans, slices at or above it averaged ~10 wright
35
+ // rounds versus ~3.5 below it.
36
+ const sliceFileAdvisory = 12
37
+
38
+ // sliceSizeAdvisory returns the Writer allowlist size and whether the slice is
39
+ // oversized without a recorded `Size rationale:`.
40
+ func sliceSizeAdvisory(block string) (int, bool) {
41
+ fields := sliceFields(block)
42
+ raw, _ := fieldValue(fields, "writer allowlist")
43
+ n := len(parsePathList(raw))
44
+ rationale, _ := fieldValue(fields, "size rationale")
45
+ return n, n >= sliceFileAdvisory && rationale == ""
46
+ }
47
+
32
48
  // sliceFields extracts the single-line `Field: value` pairs of a slice block.
33
49
  func sliceFields(block string) map[string]string {
34
50
  fields := map[string]string{}
@@ -169,6 +185,9 @@ func RunCheckSlice(root string, args []string, stdout, stderr io.Writer) int {
169
185
  problems = append(problems, "Tests/proof field missing or empty — no evidence plan for the slice")
170
186
  }
171
187
 
188
+ if n, over := sliceSizeAdvisory(block); over {
189
+ fmt.Fprintf(stdout, "check slice: ADVISORY size: %s has %d Writer allowlist files (>= %d); consider an acceptance-preserving split\n", sliceID, n, sliceFileAdvisory)
190
+ }
172
191
  if len(problems) > 0 {
173
192
  fmt.Fprintf(stdout, "check slice: %s BLOCKED\n", sliceID)
174
193
  for _, p := range problems {
@@ -127,3 +127,44 @@ func TestCheckSliceTouchedComparesNormalizedPaths(t *testing.T) {
127
127
  t.Fatalf("code=%d want 0\n%s%s", code, stdout, stderr)
128
128
  }
129
129
  }
130
+
131
+ func bigSliceTasks(rationale string) string {
132
+ var paths []string
133
+ for i := 1; i <= sliceFileAdvisory; i++ {
134
+ paths = append(paths, "`src/f"+strings.Repeat("x", i)+".go`")
135
+ }
136
+ return "# Tasks\n\n## SLICE-001 Big\nGoal: do it.\nSatisfies: AC-001\ndepends_on: []\nWriter allowlist: " +
137
+ strings.Join(paths, ", ") + "\nTests/proof: go test ./...\n" + rationale +
138
+ "\n## SLICE-002 Small\nGoal: tiny.\nSatisfies: AC-001\ndepends_on: [SLICE-001]\nWriter allowlist: `src/a.go`\nTests/proof: go test ./...\n"
139
+ }
140
+
141
+ func TestSliceSizeAdvisoryNeverBlocks(t *testing.T) {
142
+ root := sliceWorkspace(t, bigSliceTasks(""), "## Acceptance criteria\n- AC-001 works\n")
143
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
144
+ if code := RunCheckSlice(root, []string{"feat", "SLICE-001"}, stdout, stderr); code != 0 {
145
+ t.Fatalf("advisory must not block: code=%d\n%s%s", code, stdout, stderr)
146
+ }
147
+ if !strings.Contains(stdout.String(), "ADVISORY size: SLICE-001 has 12 Writer allowlist files") {
148
+ t.Fatalf("advisory missing:\n%s", stdout)
149
+ }
150
+ stdout.Reset()
151
+ if code := RunTaskGraphCheck(root, "feat", stdout, stderr); code != 0 {
152
+ t.Fatalf("task-graph code=%d\n%s%s", code, stdout, stderr)
153
+ }
154
+ out := stdout.String()
155
+ if !strings.Contains(out, "task-graph: ADVISORY size: SLICE-001 has 12") || strings.Contains(out, "SLICE-002 has") {
156
+ t.Fatalf("task-graph advisory wrong:\n%s", out)
157
+ }
158
+ stdout.Reset()
159
+ if code := RunMetrics(root, []string{"summary", "feat"}, stdout, stderr); code != 0 || !strings.Contains(stdout.String(), "size-advisory=1") {
160
+ t.Fatalf("size-advisory metric not recorded: code=%d\n%s", code, stdout)
161
+ }
162
+ }
163
+
164
+ func TestSliceSizeRationaleSilencesAdvisory(t *testing.T) {
165
+ root := sliceWorkspace(t, bigSliceTasks("Size rationale: one generated golden set moves together\n"), "## Acceptance criteria\n- AC-001 works\n")
166
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
167
+ if code := RunTaskGraphCheck(root, "feat", stdout, stderr); code != 0 || strings.Contains(stdout.String(), "ADVISORY") {
168
+ t.Fatalf("rationale must silence advisory: code=%d\n%s", code, stdout)
169
+ }
170
+ }