devrites 5.4.0 → 5.5.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 (213) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/command-map.md +2 -2
  4. package/docs/engine/state-schema.md +12 -0
  5. package/docs/flow.md +4 -0
  6. package/docs/orchestration.md +5 -2
  7. package/engine/commands.go +2 -2
  8. package/engine/internal/devritespaths/paths.go +15 -0
  9. package/engine/internal/gate/gate.go +78 -0
  10. package/engine/internal/gate/gate_test.go +118 -0
  11. package/engine/internal/lib/candidate.go +7 -3
  12. package/engine/internal/lib/manifest_union.go +326 -0
  13. package/engine/internal/lib/manifest_union_test.go +214 -0
  14. package/engine/internal/lib/observe_budgets.go +73 -0
  15. package/engine/internal/lib/observe_budgets_test.go +142 -0
  16. package/engine/internal/lib/observe_summary.go +74 -13
  17. package/engine/internal/lib/workspace_read.go +11 -6
  18. package/engine/internal/state/budgets.go +163 -0
  19. package/engine/internal/state/budgets_test.go +42 -0
  20. package/engine/internal/state/cursor.go +16 -10
  21. package/engine/internal/state/observation.go +50 -4
  22. package/engine/internal/state/status.go +38 -26
  23. package/engine/internal/state/taskgraph.go +23 -0
  24. package/engine/main.go +32 -13
  25. package/engine/root_routing_test.go +3 -1
  26. package/pack/.claude/agents/devrites-code-reviewer.md +9 -6
  27. package/pack/.claude/agents/devrites-doubt-reviewer.md +6 -0
  28. package/pack/.claude/agents/devrites-plan-reviewer.md +7 -1
  29. package/pack/.claude/agents/devrites-slice-wright.md +42 -46
  30. package/pack/.claude/agents/devrites-test-analyst.md +9 -4
  31. package/pack/.claude/agents/devrites-upgrade-planner.md +9 -1
  32. package/pack/.claude/settings.json +17 -0
  33. package/pack/.claude/skills/devrites-api-interface/SKILL.md +2 -2
  34. package/pack/.claude/skills/devrites-doubt/SKILL.md +9 -17
  35. package/pack/.claude/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  36. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  37. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  38. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -0
  39. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  40. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +1 -1
  41. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  42. package/pack/.claude/skills/rite-autocomplete/SKILL.md +19 -8
  43. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +33 -1
  44. package/pack/.claude/skills/rite-build/SKILL.md +26 -19
  45. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +1 -1
  46. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +90 -40
  47. package/pack/.claude/skills/rite-build/reference/phase-contract.md +79 -59
  48. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +14 -15
  49. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +8 -8
  50. package/pack/.claude/skills/rite-define/SKILL.md +4 -1
  51. package/pack/.claude/skills/rite-plan/SKILL.md +4 -1
  52. package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +21 -0
  53. package/pack/.claude/skills/rite-plan/reference/slicing.md +117 -15
  54. package/pack/.claude/skills/rite-prove/SKILL.md +20 -22
  55. package/pack/.claude/skills/rite-review/SKILL.md +23 -16
  56. package/pack/.claude/skills/rite-seal/SKILL.md +8 -8
  57. package/pack/.claude/skills/rite-seal/reference/final-evidence.md +6 -5
  58. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +13 -13
  59. package/pack/.claude/skills/rite-ship/SKILL.md +3 -0
  60. package/pack/.claude/skills/rite-ship/reference/close-out.md +16 -2
  61. package/pack/.claude/skills/rite-ship/reference/git-ship.md +14 -5
  62. package/pack/.claude/skills/rite-spec/SKILL.md +6 -0
  63. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +6 -0
  64. package/pack/.claude/skills/rite-status/SKILL.md +4 -1
  65. package/pack/.claude/skills/rite-temper/SKILL.md +14 -11
  66. package/pack/.claude/skills/rite-temper/reference/anti-patterns.md +14 -13
  67. package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +3 -4
  68. package/pack/.claude/skills/rite-upgrade/SKILL.md +11 -2
  69. package/pack/.claude/skills/rite-vet/SKILL.md +22 -15
  70. package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +11 -9
  71. package/pack/.claude/skills/rite-vet/reference/depth.md +6 -7
  72. package/pack/.claude/skills/rite-vet/reference/review-axes.md +39 -14
  73. package/pack/generated/claude/agents/devrites-code-reviewer.md +9 -6
  74. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +6 -0
  75. package/pack/generated/claude/agents/devrites-plan-reviewer.md +7 -1
  76. package/pack/generated/claude/agents/devrites-slice-wright.md +42 -46
  77. package/pack/generated/claude/agents/devrites-test-analyst.md +9 -4
  78. package/pack/generated/claude/agents/devrites-upgrade-planner.md +9 -1
  79. package/pack/generated/claude/settings.json +17 -0
  80. package/pack/generated/claude/skills/devrites-api-interface/SKILL.md +2 -2
  81. package/pack/generated/claude/skills/devrites-doubt/SKILL.md +9 -17
  82. package/pack/generated/claude/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  83. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  84. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  85. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -0
  86. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  87. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +1 -1
  88. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  89. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +19 -8
  90. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +33 -1
  91. package/pack/generated/claude/skills/rite-build/SKILL.md +26 -19
  92. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +1 -1
  93. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +90 -40
  94. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +79 -59
  95. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +14 -15
  96. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +8 -8
  97. package/pack/generated/claude/skills/rite-define/SKILL.md +4 -1
  98. package/pack/generated/claude/skills/rite-plan/SKILL.md +4 -1
  99. package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +21 -0
  100. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +117 -15
  101. package/pack/generated/claude/skills/rite-prove/SKILL.md +20 -22
  102. package/pack/generated/claude/skills/rite-review/SKILL.md +23 -16
  103. package/pack/generated/claude/skills/rite-seal/SKILL.md +8 -8
  104. package/pack/generated/claude/skills/rite-seal/reference/final-evidence.md +6 -5
  105. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +13 -13
  106. package/pack/generated/claude/skills/rite-ship/SKILL.md +3 -0
  107. package/pack/generated/claude/skills/rite-ship/reference/close-out.md +16 -2
  108. package/pack/generated/claude/skills/rite-ship/reference/git-ship.md +14 -5
  109. package/pack/generated/claude/skills/rite-spec/SKILL.md +6 -0
  110. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +6 -0
  111. package/pack/generated/claude/skills/rite-status/SKILL.md +4 -1
  112. package/pack/generated/claude/skills/rite-temper/SKILL.md +14 -11
  113. package/pack/generated/claude/skills/rite-temper/reference/anti-patterns.md +14 -13
  114. package/pack/generated/claude/skills/rite-temper/reference/review-dimensions.md +3 -4
  115. package/pack/generated/claude/skills/rite-upgrade/SKILL.md +11 -2
  116. package/pack/generated/claude/skills/rite-vet/SKILL.md +22 -15
  117. package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +11 -9
  118. package/pack/generated/claude/skills/rite-vet/reference/depth.md +6 -7
  119. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +39 -14
  120. package/pack/generated/codex/agents/devrites-code-reviewer.toml +9 -6
  121. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +6 -0
  122. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +7 -1
  123. package/pack/generated/codex/agents/devrites-slice-wright.toml +42 -46
  124. package/pack/generated/codex/agents/devrites-test-analyst.toml +9 -4
  125. package/pack/generated/codex/agents/devrites-upgrade-planner.toml +9 -1
  126. package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +2 -2
  127. package/pack/generated/codex/skills/devrites-doubt/SKILL.md +9 -17
  128. package/pack/generated/codex/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  129. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  130. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  131. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -0
  132. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  133. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +1 -1
  134. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  135. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +19 -8
  136. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +33 -1
  137. package/pack/generated/codex/skills/rite-build/SKILL.md +26 -19
  138. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +1 -1
  139. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +90 -40
  140. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +79 -59
  141. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +14 -15
  142. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +8 -8
  143. package/pack/generated/codex/skills/rite-define/SKILL.md +4 -1
  144. package/pack/generated/codex/skills/rite-plan/SKILL.md +4 -1
  145. package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +21 -0
  146. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +117 -15
  147. package/pack/generated/codex/skills/rite-prove/SKILL.md +20 -22
  148. package/pack/generated/codex/skills/rite-review/SKILL.md +23 -16
  149. package/pack/generated/codex/skills/rite-seal/SKILL.md +8 -8
  150. package/pack/generated/codex/skills/rite-seal/reference/final-evidence.md +6 -5
  151. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +13 -13
  152. package/pack/generated/codex/skills/rite-ship/SKILL.md +3 -0
  153. package/pack/generated/codex/skills/rite-ship/reference/close-out.md +16 -2
  154. package/pack/generated/codex/skills/rite-ship/reference/git-ship.md +14 -5
  155. package/pack/generated/codex/skills/rite-spec/SKILL.md +6 -0
  156. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +6 -0
  157. package/pack/generated/codex/skills/rite-status/SKILL.md +4 -1
  158. package/pack/generated/codex/skills/rite-temper/SKILL.md +14 -11
  159. package/pack/generated/codex/skills/rite-temper/reference/anti-patterns.md +14 -13
  160. package/pack/generated/codex/skills/rite-temper/reference/review-dimensions.md +3 -4
  161. package/pack/generated/codex/skills/rite-upgrade/SKILL.md +11 -2
  162. package/pack/generated/codex/skills/rite-vet/SKILL.md +22 -15
  163. package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +11 -9
  164. package/pack/generated/codex/skills/rite-vet/reference/depth.md +6 -7
  165. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +39 -14
  166. package/pack/generated/omp/agents/devrites-code-reviewer.md +9 -6
  167. package/pack/generated/omp/agents/devrites-doubt-reviewer.md +6 -0
  168. package/pack/generated/omp/agents/devrites-plan-reviewer.md +7 -1
  169. package/pack/generated/omp/agents/devrites-slice-wright.md +42 -46
  170. package/pack/generated/omp/agents/devrites-test-analyst.md +9 -4
  171. package/pack/generated/omp/agents/devrites-upgrade-planner.md +9 -1
  172. package/pack/generated/omp/skills/devrites-api-interface/SKILL.md +2 -2
  173. package/pack/generated/omp/skills/devrites-doubt/SKILL.md +9 -17
  174. package/pack/generated/omp/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  175. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  176. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  177. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -0
  178. package/pack/generated/omp/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  179. package/pack/generated/omp/skills/devrites-lib/reference/standards/core.md +1 -1
  180. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  181. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +19 -8
  182. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +33 -1
  183. package/pack/generated/omp/skills/rite-build/SKILL.md +26 -19
  184. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +1 -1
  185. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +90 -40
  186. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +79 -59
  187. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +14 -15
  188. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +8 -8
  189. package/pack/generated/omp/skills/rite-define/SKILL.md +4 -1
  190. package/pack/generated/omp/skills/rite-plan/SKILL.md +4 -1
  191. package/pack/generated/omp/skills/rite-plan/reference/replan-and-repair.md +21 -0
  192. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +117 -15
  193. package/pack/generated/omp/skills/rite-prove/SKILL.md +20 -22
  194. package/pack/generated/omp/skills/rite-review/SKILL.md +23 -16
  195. package/pack/generated/omp/skills/rite-seal/SKILL.md +8 -8
  196. package/pack/generated/omp/skills/rite-seal/reference/final-evidence.md +6 -5
  197. package/pack/generated/omp/skills/rite-seal/reference/phase-contract.md +13 -13
  198. package/pack/generated/omp/skills/rite-ship/SKILL.md +3 -0
  199. package/pack/generated/omp/skills/rite-ship/reference/close-out.md +16 -2
  200. package/pack/generated/omp/skills/rite-ship/reference/git-ship.md +14 -5
  201. package/pack/generated/omp/skills/rite-spec/SKILL.md +6 -0
  202. package/pack/generated/omp/skills/rite-spec/reference/state-workspace.md +6 -0
  203. package/pack/generated/omp/skills/rite-status/SKILL.md +4 -1
  204. package/pack/generated/omp/skills/rite-temper/SKILL.md +14 -11
  205. package/pack/generated/omp/skills/rite-temper/reference/anti-patterns.md +14 -13
  206. package/pack/generated/omp/skills/rite-temper/reference/review-dimensions.md +3 -4
  207. package/pack/generated/omp/skills/rite-upgrade/SKILL.md +11 -2
  208. package/pack/generated/omp/skills/rite-vet/SKILL.md +22 -15
  209. package/pack/generated/omp/skills/rite-vet/reference/anti-patterns.md +11 -9
  210. package/pack/generated/omp/skills/rite-vet/reference/depth.md +6 -7
  211. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +39 -14
  212. package/package.json +1 -1
  213. package/scripts/codex-generate.sh +6 -0
@@ -0,0 +1,142 @@
1
+ package lib
2
+
3
+ import (
4
+ "bytes"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/devrites/devrites/internal/state"
11
+ )
12
+
13
+ const budgetTasks = `# Tasks
14
+
15
+ Current checkpoint: intro text.
16
+
17
+ ## SLICE-001 First
18
+ Goal: one
19
+ Dependencies: none
20
+ depends_on: []
21
+
22
+ ### Notes
23
+ nested heading stays inside the slice
24
+
25
+ ## SLICE-002 Second
26
+ Goal: two
27
+ Dependencies: SLICE-001
28
+ depends_on: [SLICE-001]
29
+
30
+ ## Trailing section
31
+ not a slice
32
+ `
33
+
34
+ func writeBudgetWorkspace(t *testing.T, files map[string]string) (root, workspace string) {
35
+ t.Helper()
36
+ root = filepath.Join(t.TempDir(), ".devrites")
37
+ workspace = filepath.Join(root, "work", "feature")
38
+ if err := os.MkdirAll(workspace, 0o755); err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ for name, body := range files {
42
+ if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o644); err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ }
46
+ return root, workspace
47
+ }
48
+
49
+ func TestExtractTaskSliceStopsAtNextH2AndKeepsH3(t *testing.T) {
50
+ block, ok := state.ExtractTaskSlice([]byte(budgetTasks), "SLICE-001")
51
+ if !ok {
52
+ t.Fatal("expected SLICE-001")
53
+ }
54
+ if !strings.HasPrefix(block, "## SLICE-001 First\n") || !strings.Contains(block, "### Notes") {
55
+ t.Fatalf("block=%q", block)
56
+ }
57
+ if strings.Contains(block, "SLICE-002") || strings.Contains(block, "Trailing") {
58
+ t.Fatalf("block leaked past the next heading: %q", block)
59
+ }
60
+ last, ok := state.ExtractTaskSlice([]byte(budgetTasks), "SLICE-002")
61
+ if !ok || strings.Contains(last, "Trailing section") {
62
+ t.Fatalf("last=%q ok=%v", last, ok)
63
+ }
64
+ if _, ok := state.ExtractTaskSlice([]byte(budgetTasks), "SLICE-003"); ok {
65
+ t.Fatal("SLICE-003 must not resolve")
66
+ }
67
+ }
68
+
69
+ func TestRunObserveSlicePrintsOneSection(t *testing.T) {
70
+ root, _ := writeBudgetWorkspace(t, map[string]string{"tasks.md": budgetTasks})
71
+ var stdout, stderr bytes.Buffer
72
+ if code := RunObserveSlice(root, "feature", "SLICE-002", &stdout, &stderr); code != 0 {
73
+ t.Fatalf("code=%d stderr=%s", code, stderr.String())
74
+ }
75
+ if got := stdout.String(); !strings.HasPrefix(got, "## SLICE-002 Second\n") || strings.Contains(got, "SLICE-001 First") {
76
+ t.Fatalf("stdout=%q", got)
77
+ }
78
+
79
+ stdout.Reset()
80
+ if code := RunObserveSlice(root, "feature", "SLICE-009", &stdout, &stderr); code != 3 || !strings.Contains(stdout.String(), "BLOCKED") {
81
+ t.Fatalf("code=%d stdout=%q", code, stdout.String())
82
+ }
83
+ if code := RunObserveSlice(root, "feature", "slice-1", &stdout, &stderr); code != 2 {
84
+ t.Fatalf("code=%d for malformed id", code)
85
+ }
86
+ }
87
+
88
+ func TestObserveSummaryReportsBudgetsAndBulkFiles(t *testing.T) {
89
+ longLine := strings.Repeat("x", 60_000) + "\n"
90
+ root, _ := writeBudgetWorkspace(t, map[string]string{
91
+ "state.md": "| phase | build |\n| schema | 3 |\n" + longLine,
92
+ "tasks.md": budgetTasks,
93
+ "vet-review-input-1.json": strings.Repeat("{}", 40_000),
94
+ "small-note.json": "{}",
95
+ })
96
+ summary, err := ObserveSummaryFor(root, "feature")
97
+ if err != nil {
98
+ t.Fatal(err)
99
+ }
100
+ byFile := map[string]ArtifactBudget{}
101
+ for _, b := range summary.ArtifactBudgets {
102
+ byFile[b.File] = b
103
+ }
104
+ if st, ok := byFile["state.md"]; !ok || !st.Over || st.Lines != 3 || st.ByteBudget != 120*state.BudgetBytesPerLine {
105
+ t.Fatalf("state.md budget=%+v ok=%v (long line must trip the byte budget)", st, ok)
106
+ }
107
+ if tk, ok := byFile["tasks.md"]; !ok || tk.Over {
108
+ t.Fatalf("tasks.md budget=%+v ok=%v", tk, ok)
109
+ }
110
+ if summary.TaskGraph == nil || len(summary.TaskGraph.Slices) != 2 ||
111
+ summary.TaskGraph.Slices[0].ID != "SLICE-001" || len(summary.TaskGraph.Slices[0].DependsOn) != 0 ||
112
+ summary.TaskGraph.Slices[1].DependsOn[0] != "SLICE-001" {
113
+ t.Fatalf("task_graph.slices=%+v", summary.TaskGraph)
114
+ }
115
+ if len(summary.BulkFiles) != 1 || summary.BulkFiles[0].File != "vet-review-input-1.json" {
116
+ t.Fatalf("bulk_files=%+v", summary.BulkFiles)
117
+ }
118
+ }
119
+
120
+ func TestObserveSummaryReportsSequenceCursor(t *testing.T) {
121
+ root, _ := writeBudgetWorkspace(t, map[string]string{
122
+ "state.md": "| phase | build |\n| schema | 3 |\n| sequence_parent | feat-1 |\n| sequence_position | 2 |\n| sequence_workspaces_remaining | 3 |\n",
123
+ })
124
+ summary, err := ObserveSummaryFor(root, "feature")
125
+ if err != nil {
126
+ t.Fatal(err)
127
+ }
128
+ seq := summary.Sequence
129
+ if seq == nil || seq.Parent != "feat-1" || seq.Position != "2" || seq.WorkspacesRemaining != "3" {
130
+ t.Fatalf("sequence=%+v", seq)
131
+ }
132
+ root, _ = writeBudgetWorkspace(t, map[string]string{
133
+ "state.md": "| phase | build |\n| schema | 3 |\n",
134
+ })
135
+ summary, err = ObserveSummaryFor(root, "feature")
136
+ if err != nil {
137
+ t.Fatal(err)
138
+ }
139
+ if summary.Sequence != nil {
140
+ t.Fatalf("sequence must be absent without cursor fields: %+v", summary.Sequence)
141
+ }
142
+ }
@@ -2,30 +2,50 @@ package lib
2
2
 
3
3
  import (
4
4
  "encoding/json"
5
+ "fmt"
5
6
  "io"
6
7
 
8
+ "github.com/devrites/devrites/internal/devritespaths"
7
9
  "github.com/devrites/devrites/internal/state"
8
10
  )
9
11
 
12
+ // ObserveSlice is one slice's identity and ordering edges, enough for a root to pick
13
+ // the next buildable slice without loading tasks.md.
14
+ type ObserveSlice struct {
15
+ ID string `json:"id"`
16
+ DependsOn []string `json:"depends_on"`
17
+ }
18
+
10
19
  // ObserveTaskGraph is the slice-graph subset of ObserveSummary.
11
20
  type ObserveTaskGraph struct {
12
- SliceCount int `json:"slice_count"`
13
- Cycle []string `json:"cycle,omitempty"`
14
- Unknown []string `json:"unknown_dependencies,omitempty"`
15
- Problems []string `json:"problems,omitempty"`
16
- OK bool `json:"ok"`
21
+ SliceCount int `json:"slice_count"`
22
+ Slices []ObserveSlice `json:"slices,omitempty"`
23
+ Cycle []string `json:"cycle,omitempty"`
24
+ Unknown []string `json:"unknown_dependencies,omitempty"`
25
+ Problems []string `json:"problems,omitempty"`
26
+ OK bool `json:"ok"`
17
27
  }
18
28
 
19
29
  // ObserveSummary is a sanitized, machine-readable workspace snapshot.
30
+ type ObserveSequenceCursor struct {
31
+ Parent string `json:"parent"`
32
+ Position string `json:"position"`
33
+ WorkspacesRemaining string `json:"workspaces_remaining"`
34
+ Role string `json:"role,omitempty"`
35
+ }
36
+
20
37
  type ObserveSummary struct {
21
- Slug string `json:"slug"`
22
- Phase string `json:"phase,omitempty"`
23
- Status string `json:"status,omitempty"`
24
- NextAction string `json:"next_action,omitempty"`
25
- MissingSections []string `json:"missing_sections,omitempty"`
26
- MissingFiles []string `json:"missing_files,omitempty"`
27
- PrinciplesPresent bool `json:"principles_present"`
28
- TaskGraph *ObserveTaskGraph `json:"task_graph,omitempty"`
38
+ Slug string `json:"slug"`
39
+ Phase string `json:"phase,omitempty"`
40
+ Status string `json:"status,omitempty"`
41
+ NextAction string `json:"next_action,omitempty"`
42
+ MissingSections []string `json:"missing_sections,omitempty"`
43
+ MissingFiles []string `json:"missing_files,omitempty"`
44
+ PrinciplesPresent bool `json:"principles_present"`
45
+ TaskGraph *ObserveTaskGraph `json:"task_graph,omitempty"`
46
+ ArtifactBudgets []ArtifactBudget `json:"artifact_budgets,omitempty"`
47
+ BulkFiles []BulkFile `json:"bulk_files,omitempty"`
48
+ Sequence *ObserveSequenceCursor `json:"sequence,omitempty"`
29
49
  }
30
50
 
31
51
  // ObserveSummaryFor builds a summary for one feature slug.
@@ -45,17 +65,58 @@ func ObserveSummaryFor(root, slug string) (ObserveSummary, error) {
45
65
  }
46
66
 
47
67
  if graph, graphErr := CheckTaskGraph(root, slug); graphErr == nil && (len(graph.Slices) > 0 || len(graph.Problems) > 0) {
68
+ slices := make([]ObserveSlice, 0, len(graph.Slices))
69
+ for _, slice := range graph.Slices {
70
+ deps := make([]string, 0, len(slice.Dependencies))
71
+ deps = append(deps, slice.Dependencies...)
72
+ slices = append(slices, ObserveSlice{ID: slice.ID, DependsOn: deps})
73
+ }
48
74
  summary.TaskGraph = &ObserveTaskGraph{
49
75
  SliceCount: len(graph.Slices),
76
+ Slices: slices,
50
77
  Cycle: append([]string(nil), graph.Cycle...),
51
78
  Unknown: append([]string(nil), graph.Unknown...),
52
79
  Problems: append([]string(nil), graph.Problems...),
53
80
  OK: len(graph.Problems) == 0,
54
81
  }
55
82
  }
83
+ if report.SequenceParent != "" || report.SequencePosition != "" || report.SequenceWorkspacesRemaining != "" || report.SequenceRole != "" {
84
+ summary.Sequence = &ObserveSequenceCursor{
85
+ Parent: report.SequenceParent,
86
+ Position: report.SequencePosition,
87
+ WorkspacesRemaining: report.SequenceWorkspacesRemaining,
88
+ Role: report.SequenceRole,
89
+ }
90
+ }
91
+ if workspace, wsErr := devritespaths.ExistingFeatureDirChecked(root, slug); wsErr == nil {
92
+ if budgets, bulk, budgetErr := observeArtifactBudgets(workspace); budgetErr == nil {
93
+ summary.ArtifactBudgets = budgets
94
+ summary.BulkFiles = bulk
95
+ }
96
+ }
56
97
  return summary, nil
57
98
  }
58
99
 
100
+ // RunObserveSlice prints one SLICE-### section of tasks.md for a slug.
101
+ func RunObserveSlice(root, slug, id string, stdout, stderr io.Writer) int {
102
+ if !state.ValidSliceID(id) {
103
+ fmt.Fprintf(stderr, "observe slice: %q is not a SLICE-### id\n", id)
104
+ return 2
105
+ }
106
+ raw, err := readWorkspaceArtifact(root, slug, "tasks.md")
107
+ if err != nil {
108
+ fmt.Fprintf(stderr, "observe slice: %v\n", err)
109
+ return 2
110
+ }
111
+ block, ok := state.ExtractTaskSlice(raw, id)
112
+ if !ok {
113
+ fmt.Fprintf(stdout, "observe slice: BLOCKED: %s not found in tasks.md\n", id)
114
+ return 3
115
+ }
116
+ fmt.Fprint(stdout, block)
117
+ return 0
118
+ }
119
+
59
120
  func missingSectionNames(sections []state.Section) []string {
60
121
  if len(sections) == 0 {
61
122
  return nil
@@ -8,23 +8,28 @@ import (
8
8
  "github.com/devrites/devrites/internal/devritespaths"
9
9
  )
10
10
 
11
+ // workspaceArtifactLimit bounds one artifact read. Slice extraction must keep
12
+ // working on a workspace whose tasks.md is over budget: the budget gate flags the
13
+ // file, while Build still reads its own slice instead of the whole document.
14
+ const workspaceArtifactLimit = 16 << 20
15
+
11
16
  func readWorkspaceArtifact(root, slug, name string) ([]byte, error) {
12
17
  workspace, err := devritespaths.ExistingFeatureDirChecked(root, slug)
13
18
  if err != nil {
14
- return nil, fmt.Errorf("task-graph: workspace unavailable: %w", err)
19
+ return nil, fmt.Errorf("workspace artifact: workspace unavailable: %w", err)
15
20
  }
16
21
  path := filepath.Join(workspace, name)
17
22
  info, err := os.Stat(path)
18
23
  if err != nil {
19
- return nil, fmt.Errorf("task-graph: %s unavailable: %w", name, err)
24
+ return nil, fmt.Errorf("workspace artifact: %s unavailable: %w", name, err)
20
25
  }
21
- if info.Size() > 1<<20 {
22
- return nil, fmt.Errorf("task-graph: %s exceeds 1 MiB limit", name)
26
+ if info.Size() > workspaceArtifactLimit {
27
+ return nil, fmt.Errorf("workspace artifact: %s exceeds %d MiB limit", name, workspaceArtifactLimit>>20)
23
28
  }
24
- // #nosec G304 -- workspace task-graph file; 1 MiB cap checked above
29
+ // #nosec G304 -- workspace artifact; size cap checked above
25
30
  raw, err := os.ReadFile(path)
26
31
  if err != nil {
27
- return nil, fmt.Errorf("task-graph: cannot read %s: %w", name, err)
32
+ return nil, fmt.Errorf("workspace artifact: cannot read %s: %w", name, err)
28
33
  }
29
34
  return raw, nil
30
35
  }
@@ -0,0 +1,163 @@
1
+ package state
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ )
7
+
8
+ // Artifact budgets mirror workspace-artifact-schema.md "What each file owns".
9
+ // A line budget also bounds bytes at BudgetBytesPerLine per budgeted line so a
10
+ // few very long lines cannot evade it.
11
+ const (
12
+ BudgetBytesPerLine = 400
13
+ // BudgetBlockFactor is the material-overshoot threshold. At or above this
14
+ // multiple of a line or byte budget a gate blocks unless the artifact
15
+ // records a structural `Budget override:` line; smaller overshoots are
16
+ // reported by `orient` and stay advisory.
17
+ BudgetBlockFactor = 1.5
18
+ // MaxArtifactBytes is the per-artifact read cap. A larger artifact cannot be
19
+ // observed and blocks as file_too_large, so callers that explain a size
20
+ // problem quote the same number the observer enforced.
21
+ MaxArtifactBytes = maxArtifactBytes
22
+ // PacketMaxBytes is the serialized-packet ceiling from context-hygiene.md:
23
+ // "A serialized packet stays <= 64 KiB; larger means the scope is wrong".
24
+ // A non-canonical workspace-root file over it is unsanctioned placement.
25
+ PacketMaxBytes = 64 << 10
26
+ )
27
+
28
+ var artifactLineBudgets = map[string]int{
29
+ "README.md": 120,
30
+ "brief.md": 80,
31
+ "spec.md": 260,
32
+ "decision-coverage.md": 200,
33
+ "strategy.md": 180,
34
+ "architecture.md": 180,
35
+ "flows.md": 160,
36
+ "decisions.md": 200,
37
+ "assumptions.md": 160,
38
+ "questions.md": 180,
39
+ "plan.md": 220,
40
+ "tasks.md": 280,
41
+ "traceability.md": 220,
42
+ "eng-review.md": 240,
43
+ "test-plan.md": 260,
44
+ "state.md": 120,
45
+ "evidence.md": 280,
46
+ "browser-evidence.md": 220,
47
+ "drift.md": 160,
48
+ "touched-files.md": 160,
49
+ "design-brief.md": 160,
50
+ "handoff.md": 120,
51
+ "polish-report.md": 200,
52
+ "ship.md": 120,
53
+ "review.md": 240,
54
+ "seal.md": 200,
55
+ "ai-spec.md": 160,
56
+ }
57
+
58
+ // advisoryOnlyArtifacts are append-only proof ledgers: they scale with proof
59
+ // volume, not with planning surface, so their budget stays advisory in `orient`
60
+ // and never blocks a gate. touched-files.md keeps its own deterministic
61
+ // manifest limits.
62
+ var advisoryOnlyArtifacts = map[string]bool{
63
+ "evidence.md": true,
64
+ "browser-evidence.md": true,
65
+ "touched-files.md": true,
66
+ }
67
+
68
+ // ArtifactLineBudget returns the documented line budget for a canonical artifact.
69
+ func ArtifactLineBudget(name string) (int, bool) {
70
+ budget, ok := artifactLineBudgets[name]
71
+ return budget, ok
72
+ }
73
+
74
+ // sanctionedRootFiles are workspace-root files the schema places there without a
75
+ // line budget: conditional artifacts (`references.md`, `investigation-map.md`,
76
+ // `dogfood.md`) and the engine-owned parallel lease. Anything else in the root
77
+ // is unsanctioned placement — `packets/` and `history/` own by-reference
78
+ // packets and relocated narrative (workspace-artifact-schema.md "Required by
79
+ // phase").
80
+ var sanctionedRootFiles = map[string]bool{
81
+ "references.md": true,
82
+ "investigation-map.md": true,
83
+ "dogfood.md": true,
84
+ "parallel-lease.md": true,
85
+ }
86
+
87
+ // SanctionedRootFile reports whether name is a legitimate workspace-root file:
88
+ // a budgeted canonical artifact or a documented unbudgeted one.
89
+ func SanctionedRootFile(name string) bool {
90
+ if _, ok := artifactLineBudgets[name]; ok {
91
+ return true
92
+ }
93
+ return sanctionedRootFiles[name]
94
+ }
95
+
96
+ // ArtifactBudgetGateApplies reports whether the deterministic material-overshoot
97
+ // gate covers this artifact (canonical and not an advisory-only proof ledger).
98
+ func ArtifactBudgetGateApplies(name string) bool {
99
+ _, ok := artifactLineBudgets[name]
100
+ return ok && !advisoryOnlyArtifacts[name]
101
+ }
102
+
103
+ // ArtifactBudgetStatus is one canonical artifact measured against its budget.
104
+ type ArtifactBudgetStatus struct {
105
+ File string
106
+ Lines int
107
+ Bytes int64
108
+ LineBudget int
109
+ ByteBudget int64
110
+ Over bool
111
+ Material bool
112
+ Override bool
113
+ }
114
+
115
+ // ArtifactBudget measures raw against the named artifact's budget. ok is false
116
+ // for a non-canonical file name.
117
+ func ArtifactBudget(name string, raw []byte) (ArtifactBudgetStatus, bool) {
118
+ lineBudget, ok := artifactLineBudgets[name]
119
+ if !ok {
120
+ return ArtifactBudgetStatus{}, false
121
+ }
122
+ status := ArtifactBudgetStatus{
123
+ File: name,
124
+ Lines: CountLines(raw),
125
+ Bytes: int64(len(raw)),
126
+ LineBudget: lineBudget,
127
+ ByteBudget: int64(lineBudget) * BudgetBytesPerLine,
128
+ }
129
+ status.Over = status.Lines > lineBudget || status.Bytes > status.ByteBudget
130
+ status.Material = float64(status.Lines) >= float64(lineBudget)*BudgetBlockFactor ||
131
+ float64(status.Bytes) >= float64(status.ByteBudget)*BudgetBlockFactor
132
+ status.Override = HasBudgetOverride(raw)
133
+ return status, true
134
+ }
135
+
136
+ // CountLines counts newline-terminated lines plus a final unterminated line.
137
+ func CountLines(raw []byte) int {
138
+ if len(raw) == 0 {
139
+ return 0
140
+ }
141
+ lines := bytes.Count(raw, []byte{'\n'})
142
+ if raw[len(raw)-1] != '\n' {
143
+ lines++
144
+ }
145
+ return lines
146
+ }
147
+
148
+ // HasBudgetOverride reports a structural `Budget override:` line outside fenced
149
+ // code blocks; a fenced example never counts.
150
+ func HasBudgetOverride(raw []byte) bool {
151
+ fenced := false
152
+ for _, line := range strings.Split(string(raw), "\n") {
153
+ trimmed := strings.TrimSpace(line)
154
+ if strings.HasPrefix(trimmed, "```") {
155
+ fenced = !fenced
156
+ continue
157
+ }
158
+ if !fenced && strings.HasPrefix(trimmed, "Budget override:") {
159
+ return true
160
+ }
161
+ }
162
+ return false
163
+ }
@@ -0,0 +1,42 @@
1
+ package state
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestArtifactBudgetMeasuresLinesBytesAndMateriality(t *testing.T) {
9
+ if _, ok := ArtifactBudget("notes.json", []byte("x")); ok {
10
+ t.Fatal("non-canonical files have no budget")
11
+ }
12
+ within, ok := ArtifactBudget("tasks.md", []byte("# Tasks\n## SLICE-001 A\nDependencies: none\n"))
13
+ if !ok || within.Over || within.Material {
14
+ t.Fatalf("within=%+v", within)
15
+ }
16
+ over, _ := ArtifactBudget("tasks.md", []byte(strings.Repeat("x\n", 300)))
17
+ if !over.Over || over.Material {
18
+ t.Fatalf("300 lines against a 280-line budget must be over but not material: %+v", over)
19
+ }
20
+ material, _ := ArtifactBudget("tasks.md", []byte(strings.Repeat("x\n", 500)))
21
+ if !material.Material {
22
+ t.Fatalf("500 lines must be material: %+v", material)
23
+ }
24
+ longLine, _ := ArtifactBudget("state.md", []byte(strings.Repeat("y", 100_000)+"\n"))
25
+ if !longLine.Material || longLine.Lines != 1 {
26
+ t.Fatalf("one very long line must trip the byte budget: %+v", longLine)
27
+ }
28
+ }
29
+
30
+ func TestHasBudgetOverrideIgnoresFencedExamples(t *testing.T) {
31
+ if HasBudgetOverride([]byte("plain text\nBudget override: relocation pending\n")) != true {
32
+ t.Fatal("structural override line must be recognized")
33
+ }
34
+ fenced := "See the schema:\n\n```markdown\nBudget override: <reason>\n```\n\nNothing recorded.\n"
35
+ if HasBudgetOverride([]byte(fenced)) {
36
+ t.Fatal("a fenced example is not an override")
37
+ }
38
+ indented := " Budget override: table form is required\n"
39
+ if !HasBudgetOverride([]byte(indented)) {
40
+ t.Fatal("leading whitespace must not hide a real override line")
41
+ }
42
+ }
@@ -12,15 +12,19 @@ import (
12
12
  // remain local to the cursor package; normalizeCursorKey still accepts legacy
13
13
  // presentation aliases at the text boundary.
14
14
  const (
15
- CursorPhase = "phase"
16
- CursorStatus = "status"
17
- CursorNextAction = "next_action"
18
- CursorQuestionID = "question_id"
19
- CursorActiveSlice = "active_slice"
20
- CursorAFKSlicesRemaining = "afk_slices_remaining"
21
- CursorReturnPhase = "return_phase"
22
- CursorReturnNextAction = "return_next_action"
23
- CursorSchema = "schema"
15
+ CursorPhase = "phase"
16
+ CursorStatus = "status"
17
+ CursorNextAction = "next_action"
18
+ CursorQuestionID = "question_id"
19
+ CursorActiveSlice = "active_slice"
20
+ CursorAFKSlicesRemaining = "afk_slices_remaining"
21
+ CursorSequenceParent = "sequence_parent"
22
+ CursorSequencePosition = "sequence_position"
23
+ CursorSequenceWorkspacesRemain = "sequence_workspaces_remaining"
24
+ CursorSequenceRole = "sequence_role"
25
+ CursorReturnPhase = "return_phase"
26
+ CursorReturnNextAction = "return_next_action"
27
+ CursorSchema = "schema"
24
28
  )
25
29
 
26
30
  var cursorKeyAliases = map[string]string{
@@ -273,7 +277,9 @@ func isMigratableCursorKey(key string) bool {
273
277
  func canonicalCursorSpelling(key string) (string, bool) {
274
278
  normalized := normalizeCursorKey(key)
275
279
  canonicals := []string{CursorPhase, CursorStatus, CursorNextAction, CursorQuestionID,
276
- CursorActiveSlice, CursorAFKSlicesRemaining, CursorReturnPhase,
280
+ CursorActiveSlice, CursorAFKSlicesRemaining, CursorSequenceParent,
281
+ CursorSequencePosition, CursorSequenceWorkspacesRemain, CursorSequenceRole,
282
+ CursorReturnPhase,
277
283
  CursorReturnNextAction, CursorSchema}
278
284
  for _, canonical := range canonicals {
279
285
  if normalized == normalizeCursorKey(canonical) {
@@ -8,6 +8,7 @@ import (
8
8
  "os"
9
9
  "path"
10
10
  "path/filepath"
11
+ "sort"
11
12
  "strings"
12
13
 
13
14
  "github.com/devrites/devrites/internal/devritespaths"
@@ -90,9 +91,22 @@ func (f ArtifactFact) Diagnostic() (ArtifactDiagnostic, bool) {
90
91
  }
91
92
 
92
93
  type WorkspaceObservation struct {
93
- slug string
94
- facts []ArtifactFact
95
- index map[ArtifactPath]int
94
+ slug string
95
+ facts []ArtifactFact
96
+ index map[ArtifactPath]int
97
+ rootFiles []RootFile
98
+ }
99
+
100
+ // RootFile is one regular file directly inside the workspace root.
101
+ type RootFile struct {
102
+ Name string
103
+ Bytes int64
104
+ }
105
+
106
+ // RootFiles lists the workspace root's regular files so a consumer can judge
107
+ // placement without acquiring the directory itself.
108
+ func (o *WorkspaceObservation) RootFiles() []RootFile {
109
+ return append([]RootFile(nil), o.rootFiles...)
96
110
  }
97
111
 
98
112
  func (o *WorkspaceObservation) Slug() string { return o.slug }
@@ -296,7 +310,39 @@ func observeWorkspace(root, slug string, callback observationCallback) (*Workspa
296
310
  for i, fact := range facts {
297
311
  index[fact.path] = i
298
312
  }
299
- return &WorkspaceObservation{slug: slug, facts: facts, index: index}, nil
313
+ rootFiles, err := readRootFiles(roots.workspace)
314
+ if err != nil {
315
+ return nil, ObservationWorkspaceInvalid
316
+ }
317
+ return &WorkspaceObservation{slug: slug, facts: facts, index: index, rootFiles: rootFiles}, nil
318
+ }
319
+
320
+ // readRootFiles lists the workspace directory's regular entries through the
321
+ // already-verified root handle, so the list describes the same snapshot as the
322
+ // artifact facts.
323
+ func readRootFiles(root *os.Root) ([]RootFile, error) {
324
+ dir, err := root.Open(".")
325
+ if err != nil {
326
+ return nil, err
327
+ }
328
+ defer func() { _ = dir.Close() }()
329
+ entries, err := dir.ReadDir(-1)
330
+ if err != nil {
331
+ return nil, err
332
+ }
333
+ files := make([]RootFile, 0, len(entries))
334
+ for _, entry := range entries {
335
+ if !entry.Type().IsRegular() {
336
+ continue
337
+ }
338
+ info, err := entry.Info()
339
+ if err != nil {
340
+ return nil, err
341
+ }
342
+ files = append(files, RootFile{Name: entry.Name(), Bytes: info.Size()})
343
+ }
344
+ sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
345
+ return files, nil
300
346
  }
301
347
 
302
348
  func observationWorkspaceOverrideValid(root, slug string) bool {