devrites 4.2.0 → 4.4.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 (117) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +15 -0
  4. package/docs/engine/commands.md +20 -2
  5. package/docs/orchestration.md +6 -3
  6. package/engine/commands.go +1 -1
  7. package/engine/internal/lib/cli_observe.go +66 -0
  8. package/engine/internal/lib/observe_summary.go +79 -0
  9. package/engine/internal/lib/open_visual.go +254 -0
  10. package/engine/internal/lib/open_visual_test.go +280 -0
  11. package/engine/internal/lib/skilltrust.go +196 -0
  12. package/engine/internal/lib/taskgraph.go +162 -0
  13. package/engine/internal/lib/taskgraph_test.go +150 -0
  14. package/engine/internal/lib/visual_outline.go +163 -0
  15. package/engine/internal/lib/visual_outline_test.go +161 -0
  16. package/engine/internal/lib/workspace_read.go +29 -0
  17. package/engine/internal/parallel/cli.go +467 -0
  18. package/engine/internal/parallel/git.go +181 -0
  19. package/engine/internal/parallel/lease.go +244 -0
  20. package/engine/internal/parallel/ops.go +416 -0
  21. package/engine/internal/parallel/parallel_test.go +242 -0
  22. package/engine/internal/parallel/pathdisjoint.go +129 -0
  23. package/engine/internal/parallel/pathdisjoint_test.go +97 -0
  24. package/engine/internal/state/status.go +41 -16
  25. package/engine/main.go +76 -3
  26. package/engine/root_routing_test.go +50 -15
  27. package/engine/testdata/visual/open-visual-smoke.html +675 -0
  28. package/engine/testdata/visual/open-visual-smoke.outline.md +54 -0
  29. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  30. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  31. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  32. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  33. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  34. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  35. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  36. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  37. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  38. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  39. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  40. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  41. package/pack/.claude/skills/rite-build/SKILL.md +29 -36
  42. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
  43. package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  44. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  45. package/pack/.claude/skills/rite-build/reference/output.md +2 -0
  46. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
  47. package/pack/.claude/skills/rite-build/reference/phase-contract.md +6 -5
  48. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  49. package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  50. package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
  51. package/pack/.claude/skills/rite-define/SKILL.md +10 -1
  52. package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
  53. package/pack/.claude/skills/rite-explain/SKILL.md +23 -7
  54. package/pack/.claude/skills/rite-explain/reference/intake.md +11 -2
  55. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  56. package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -2
  57. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +26 -3
  58. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  59. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  60. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  61. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  62. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  63. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  64. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  65. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  66. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  67. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  68. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  69. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  70. package/pack/generated/claude/skills/rite-build/SKILL.md +29 -36
  71. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
  72. package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  73. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  74. package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
  75. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
  76. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +6 -5
  77. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  78. package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  79. package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
  80. package/pack/generated/claude/skills/rite-define/SKILL.md +10 -1
  81. package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
  82. package/pack/generated/claude/skills/rite-explain/SKILL.md +23 -7
  83. package/pack/generated/claude/skills/rite-explain/reference/intake.md +11 -2
  84. package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  85. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -2
  86. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +26 -3
  87. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  88. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  89. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  90. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  91. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  92. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  93. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  94. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  95. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  96. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  97. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  98. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  99. package/pack/generated/codex/skills/rite-build/SKILL.md +29 -36
  100. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
  101. package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  102. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
  103. package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
  104. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
  105. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +6 -5
  106. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
  107. package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
  108. package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
  109. package/pack/generated/codex/skills/rite-define/SKILL.md +10 -1
  110. package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
  111. package/pack/generated/codex/skills/rite-explain/SKILL.md +23 -7
  112. package/pack/generated/codex/skills/rite-explain/reference/intake.md +11 -2
  113. package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
  114. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -2
  115. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +26 -3
  116. package/package.json +1 -1
  117. package/scripts/install-lib.sh +1 -1
@@ -0,0 +1,242 @@
1
+ package parallel
2
+
3
+ import (
4
+ "os"
5
+ "os/exec"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ func gitOk(t *testing.T, dir string, args ...string) string {
12
+ t.Helper()
13
+ cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
14
+ cmd.Env = append(os.Environ(),
15
+ "GIT_CONFIG_NOSYSTEM=1",
16
+ "GIT_TERMINAL_PROMPT=0",
17
+ "GIT_AUTHOR_NAME=DevRites Test",
18
+ "GIT_AUTHOR_EMAIL=devrites@example.invalid",
19
+ "GIT_COMMITTER_NAME=DevRites Test",
20
+ "GIT_COMMITTER_EMAIL=devrites@example.invalid",
21
+ )
22
+ out, err := cmd.CombinedOutput()
23
+ if err != nil {
24
+ t.Fatalf("git %v: %v\n%s", args, err, out)
25
+ }
26
+ return strings.TrimSpace(string(out))
27
+ }
28
+
29
+ func setupRepo(t *testing.T) (repo, base string) {
30
+ t.Helper()
31
+ repo = t.TempDir()
32
+ gitOk(t, repo, "init")
33
+ gitOk(t, repo, "checkout", "-b", "main")
34
+ if err := os.MkdirAll(filepath.Join(repo, "src"), 0o755); err != nil {
35
+ t.Fatal(err)
36
+ }
37
+ if err := os.WriteFile(filepath.Join(repo, "src", "a.go"), []byte("package main\n"), 0o644); err != nil {
38
+ t.Fatal(err)
39
+ }
40
+ if err := os.WriteFile(filepath.Join(repo, "src", "b.go"), []byte("package main\n"), 0o644); err != nil {
41
+ t.Fatal(err)
42
+ }
43
+ if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("base\n"), 0o644); err != nil {
44
+ t.Fatal(err)
45
+ }
46
+ gitOk(t, repo, "add", ".")
47
+ gitOk(t, repo, "commit", "-m", "base")
48
+ base = gitOk(t, repo, "rev-parse", "HEAD")
49
+ return repo, base
50
+ }
51
+
52
+ func TestLeaseRoundTrip(t *testing.T) {
53
+ t.Parallel()
54
+ dir := t.TempDir()
55
+ path := filepath.Join(dir, "parallel-lease.md")
56
+ lease := &Lease{
57
+ BatchID: "batch1",
58
+ CreatedAt: "2026-08-23T00:00:00Z",
59
+ BaseSHA: "abcdef1",
60
+ N: 2,
61
+ Status: StatusRunning,
62
+ ControlPIDOrSession: "test",
63
+ Slices: []LeaseSlice{
64
+ {ID: "slice-a", Paths: []string{"src/a.go"}, WorktreePath: "/tmp/a", Branch: "devrites/parallel/x/batch1/slice-a", WrightStatus: WrightPending},
65
+ {ID: "slice-b", Paths: []string{"src/b.go"}, WorktreePath: "/tmp/b", Branch: "devrites/parallel/x/batch1/slice-b", WrightStatus: WrightPending},
66
+ },
67
+ }
68
+ if err := WriteLease(path, lease); err != nil {
69
+ t.Fatal(err)
70
+ }
71
+ got, err := ReadLease(path)
72
+ if err != nil {
73
+ t.Fatal(err)
74
+ }
75
+ if got.BatchID != "batch1" || got.Status != StatusRunning || len(got.Slices) != 2 {
76
+ t.Fatalf("unexpected lease: %#v", got)
77
+ }
78
+ }
79
+
80
+ func TestCreateAbortCleanup(t *testing.T) {
81
+ repo, base := setupRepo(t)
82
+ slug := "demo-feature"
83
+ batch := "batch1"
84
+ lease, err := Create(CreateOpts{
85
+ Root: repo,
86
+ Slug: slug,
87
+ BatchID: batch,
88
+ BaseSHA: base,
89
+ Slices: []SlicePaths{
90
+ {ID: "slice-a", Paths: []string{"src/a.go"}},
91
+ {ID: "slice-b", Paths: []string{"src/b.go"}},
92
+ },
93
+ })
94
+ if err != nil {
95
+ t.Fatal(err)
96
+ }
97
+ if lease.Status != StatusRunning || lease.N != 2 {
98
+ t.Fatalf("lease=%#v", lease)
99
+ }
100
+ wtA := lease.Slices[0].WorktreePath
101
+ wtB := lease.Slices[1].WorktreePath
102
+ for _, wt := range []string{wtA, wtB} {
103
+ if _, err := os.Stat(wt); err != nil {
104
+ t.Fatalf("missing worktree %s: %v", wt, err)
105
+ }
106
+ if head := gitOk(t, wt, "rev-parse", "HEAD"); head != base {
107
+ t.Fatalf("worktree head %s want %s", head, base)
108
+ }
109
+ }
110
+ if head := gitOk(t, repo, "rev-parse", "HEAD"); head != base {
111
+ t.Fatalf("control moved after create: %s", head)
112
+ }
113
+
114
+ // Drift control tip, then abort should restore base.
115
+ if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("drift\n"), 0o644); err != nil {
116
+ t.Fatal(err)
117
+ }
118
+ gitOk(t, repo, "add", "README.md")
119
+ gitOk(t, repo, "commit", "-m", "drift")
120
+ if _, err := Abort(repo, slug, true); err != nil {
121
+ t.Fatal(err)
122
+ }
123
+ if head := gitOk(t, repo, "rev-parse", "HEAD"); head != base {
124
+ t.Fatalf("abort left control at %s want %s", head, base)
125
+ }
126
+ leasePath, _ := LeasePath(repo, slug)
127
+ lease, err = ReadLease(leasePath)
128
+ if err != nil {
129
+ t.Fatal(err)
130
+ }
131
+ if lease.Status != StatusAborted {
132
+ t.Fatalf("status=%s", lease.Status)
133
+ }
134
+ if _, err := os.Stat(wtA); err != nil {
135
+ t.Fatalf("abort should preserve worktree: %v", err)
136
+ }
137
+
138
+ if err := Cleanup(repo, slug, true); err != nil {
139
+ t.Fatal(err)
140
+ }
141
+ if _, err := os.Stat(wtA); !os.IsNotExist(err) {
142
+ t.Fatalf("cleanup should remove worktree A")
143
+ }
144
+ if _, err := os.Stat(leasePath); !os.IsNotExist(err) {
145
+ t.Fatalf("cleanup should clear lease")
146
+ }
147
+ }
148
+
149
+ func TestCreateRefusesOverlap(t *testing.T) {
150
+ repo, base := setupRepo(t)
151
+ _, err := Create(CreateOpts{
152
+ Root: repo,
153
+ Slug: "demo",
154
+ BatchID: "batch1",
155
+ BaseSHA: base,
156
+ Slices: []SlicePaths{
157
+ {ID: "slice-a", Paths: []string{"src/a.go"}},
158
+ {ID: "slice-b", Paths: []string{"src/a.go"}},
159
+ },
160
+ })
161
+ if err == nil || !strings.Contains(err.Error(), "overlap") {
162
+ t.Fatalf("expected overlap refusal, got %v", err)
163
+ }
164
+ }
165
+
166
+ func TestIntegrateDivergentSiblings(t *testing.T) {
167
+ repo, base := setupRepo(t)
168
+ slug := "demo-feature"
169
+ batch := "batch1"
170
+ lease, err := Create(CreateOpts{
171
+ Root: repo,
172
+ Slug: slug,
173
+ BatchID: batch,
174
+ BaseSHA: base,
175
+ Slices: []SlicePaths{
176
+ {ID: "slice-a", Paths: []string{"src/a.go"}},
177
+ {ID: "slice-b", Paths: []string{"src/b.go"}},
178
+ },
179
+ })
180
+ if err != nil {
181
+ t.Fatal(err)
182
+ }
183
+ wtA := lease.Slices[0].WorktreePath
184
+ wtB := lease.Slices[1].WorktreePath
185
+
186
+ if err := os.WriteFile(filepath.Join(wtA, "src", "a.go"), []byte("package main\n\nfunc A() {}\n"), 0o644); err != nil {
187
+ t.Fatal(err)
188
+ }
189
+ gitOk(t, wtA, "add", "src/a.go")
190
+ gitOk(t, wtA, "commit", "-m", "slice-a green")
191
+ tcA := gitOk(t, wtA, "rev-parse", "HEAD")
192
+
193
+ if err := os.WriteFile(filepath.Join(wtB, "src", "b.go"), []byte("package main\n\nfunc B() {}\n"), 0o644); err != nil {
194
+ t.Fatal(err)
195
+ }
196
+ gitOk(t, wtB, "add", "src/b.go")
197
+ gitOk(t, wtB, "commit", "-m", "slice-b green")
198
+ tcB := gitOk(t, wtB, "rev-parse", "HEAD")
199
+
200
+ if _, err := RecordGreen(repo, slug, "slice-a", tcA); err != nil {
201
+ t.Fatal(err)
202
+ }
203
+ if _, err := RecordGreen(repo, slug, "slice-b", tcB); err != nil {
204
+ t.Fatal(err)
205
+ }
206
+
207
+ tip, lease, err := Integrate(IntegrateOpts{Root: repo, Slug: slug, ApplyToControl: true})
208
+ if err != nil {
209
+ t.Fatal(err)
210
+ }
211
+ if lease.Status != StatusComplete {
212
+ t.Fatalf("status=%s", lease.Status)
213
+ }
214
+ if tip == "" || tip == base {
215
+ t.Fatalf("expected advanced tip, got %s", tip)
216
+ }
217
+ head := gitOk(t, repo, "rev-parse", "HEAD")
218
+ if head != tip {
219
+ t.Fatalf("control head %s want tip %s", head, tip)
220
+ }
221
+ a := mustRead(t, filepath.Join(repo, "src", "a.go"))
222
+ b := mustRead(t, filepath.Join(repo, "src", "b.go"))
223
+ if !strings.Contains(a, "func A") || !strings.Contains(b, "func B") {
224
+ t.Fatalf("integrated contents missing: a=%q b=%q", a, b)
225
+ }
226
+
227
+ if err := Cleanup(repo, slug, false); err != nil {
228
+ t.Fatal(err)
229
+ }
230
+ if _, err := os.Stat(wtA); !os.IsNotExist(err) {
231
+ t.Fatalf("cleanup should remove workers")
232
+ }
233
+ }
234
+
235
+ func mustRead(t *testing.T, path string) string {
236
+ t.Helper()
237
+ b, err := os.ReadFile(path)
238
+ if err != nil {
239
+ t.Fatal(err)
240
+ }
241
+ return string(b)
242
+ }
@@ -0,0 +1,129 @@
1
+ // Package parallel implements deterministic parallel-worktree control ops:
2
+ // path-disjoint checks, advisory leases, and git worktree create/integrate/cleanup.
3
+ package parallel
4
+
5
+ import (
6
+ "encoding/json"
7
+ "fmt"
8
+ "os"
9
+ "path/filepath"
10
+ "regexp"
11
+ "sort"
12
+ "strings"
13
+ )
14
+
15
+ var winAbsRE = regexp.MustCompile(`(?i)^[A-Za-z]:[/\\]`)
16
+
17
+ // SlicePaths is one path-disjoint eligibility unit.
18
+ type SlicePaths struct {
19
+ ID string `json:"id"`
20
+ Paths []string `json:"paths"`
21
+ }
22
+
23
+ // NormalizePath mirrors scripts/check-path-disjoint.py: slash-normalize, reject
24
+ // empty/absolute/.. paths, and drop empty/"." segments.
25
+ func NormalizePath(raw string) (string, error) {
26
+ path := strings.TrimSpace(strings.ReplaceAll(raw, `\`, "/"))
27
+ if path == "" {
28
+ return "", fmt.Errorf("empty path is not allowed")
29
+ }
30
+ if strings.HasPrefix(path, "/") || winAbsRE.MatchString(path) {
31
+ return "", fmt.Errorf("path must be project-relative, not absolute: %q", raw)
32
+ }
33
+ parts := make([]string, 0, strings.Count(path, "/")+1)
34
+ for part := range strings.SplitSeq(path, "/") {
35
+ if part == "" || part == "." {
36
+ continue
37
+ }
38
+ if part == ".." {
39
+ return "", fmt.Errorf("path must not contain '..': %q", raw)
40
+ }
41
+ parts = append(parts, part)
42
+ }
43
+ if len(parts) == 0 {
44
+ return "", fmt.Errorf("empty path is not allowed")
45
+ }
46
+ return strings.Join(parts, "/"), nil
47
+ }
48
+
49
+ func validateSlicePaths(paths []string, label, root string) ([]string, error) {
50
+ if paths == nil {
51
+ return nil, fmt.Errorf("%s: paths must be a list", label)
52
+ }
53
+ normalized := make([]string, 0, len(paths))
54
+ seen := make(map[string]struct{}, len(paths))
55
+ for _, raw := range paths {
56
+ path, err := NormalizePath(raw)
57
+ if err != nil {
58
+ return nil, fmt.Errorf("%s: %w", label, err)
59
+ }
60
+ // Parallel eligibility must reject workspace control metadata so slices
61
+ // cannot claim control-tree / SSOT files (docs: ".devrites/**").
62
+ if path == ".devrites" || strings.HasPrefix(path, ".devrites/") {
63
+ return nil, fmt.Errorf("%s: path must not include .devrites: %q", label, path)
64
+ }
65
+ if _, ok := seen[path]; ok {
66
+ return nil, fmt.Errorf("%s: duplicate path %q", label, path)
67
+ }
68
+ seen[path] = struct{}{}
69
+ normalized = append(normalized, path)
70
+ if root != "" {
71
+ full := filepath.Join(root, filepath.FromSlash(path))
72
+ if info, err := os.Lstat(full); err == nil && info.Mode()&os.ModeSymlink != 0 {
73
+ return nil, fmt.Errorf("%s: symlink path is not allowed: %q", label, path)
74
+ }
75
+ }
76
+ }
77
+ return normalized, nil
78
+ }
79
+
80
+ // CheckPathDisjoint returns slice ids when every pair of path sets is disjoint.
81
+ func CheckPathDisjoint(slices []SlicePaths, root string) ([]string, error) {
82
+ if len(slices) < 2 {
83
+ return nil, fmt.Errorf("need at least two slices to check path-disjoint eligibility")
84
+ }
85
+ owners := make(map[string][]string)
86
+ ids := make([]string, 0, len(slices))
87
+ for index, item := range slices {
88
+ label := fmt.Sprintf("slice %d", index)
89
+ if item.ID != "" {
90
+ label = fmt.Sprintf("slice %q", item.ID)
91
+ ids = append(ids, item.ID)
92
+ } else {
93
+ ids = append(ids, fmt.Sprintf("%d", index))
94
+ }
95
+ paths, err := validateSlicePaths(item.Paths, label, root)
96
+ if err != nil {
97
+ return nil, err
98
+ }
99
+ for _, path := range paths {
100
+ owners[path] = append(owners[path], label)
101
+ }
102
+ }
103
+ var overlaps []string
104
+ for path, labels := range owners {
105
+ if len(labels) > 1 {
106
+ overlaps = append(overlaps, fmt.Sprintf("%q shared by %s", path, strings.Join(labels, ", ")))
107
+ }
108
+ }
109
+ if len(overlaps) > 0 {
110
+ sort.Strings(overlaps)
111
+ return nil, fmt.Errorf("path sets overlap: %s", strings.Join(overlaps, "; "))
112
+ }
113
+ return ids, nil
114
+ }
115
+
116
+ // ParseSlicesJSON accepts {"slices":[...]} or a top-level slices array.
117
+ func ParseSlicesJSON(data []byte) ([]SlicePaths, error) {
118
+ var asObject struct {
119
+ Slices []SlicePaths `json:"slices"`
120
+ }
121
+ if err := json.Unmarshal(data, &asObject); err == nil && asObject.Slices != nil {
122
+ return asObject.Slices, nil
123
+ }
124
+ var asList []SlicePaths
125
+ if err := json.Unmarshal(data, &asList); err == nil {
126
+ return asList, nil
127
+ }
128
+ return nil, fmt.Errorf(`input must be {"slices": [...]} or a top-level slices array`)
129
+ }
@@ -0,0 +1,97 @@
1
+ package parallel
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ func TestNormalizePathRejectsDirty(t *testing.T) {
11
+ t.Parallel()
12
+ cases := []string{"", " ", "/", "/abs", `C:\windows`, `..`, "a/../b", "../x"}
13
+ for _, raw := range cases {
14
+ if _, err := NormalizePath(raw); err == nil {
15
+ t.Fatalf("NormalizePath(%q) should fail", raw)
16
+ }
17
+ }
18
+ }
19
+
20
+ func TestNormalizePathAcceptsRelative(t *testing.T) {
21
+ t.Parallel()
22
+ got, err := NormalizePath(`src\foo.go`)
23
+ if err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ if got != "src/foo.go" {
27
+ t.Fatalf("got %q", got)
28
+ }
29
+ }
30
+
31
+ func TestCheckPathDisjointOverlap(t *testing.T) {
32
+ t.Parallel()
33
+ _, err := CheckPathDisjoint([]SlicePaths{
34
+ {ID: "a", Paths: []string{"src/a.go"}},
35
+ {ID: "b", Paths: []string{"src/a.go"}},
36
+ }, "")
37
+ if err == nil || !strings.Contains(err.Error(), "overlap") {
38
+ t.Fatalf("expected overlap error, got %v", err)
39
+ }
40
+ }
41
+
42
+ func TestCheckPathDisjointOK(t *testing.T) {
43
+ t.Parallel()
44
+ ids, err := CheckPathDisjoint([]SlicePaths{
45
+ {ID: "a", Paths: []string{"src/a.go"}},
46
+ {ID: "b", Paths: []string{"src/b.go"}},
47
+ }, "")
48
+ if err != nil {
49
+ t.Fatal(err)
50
+ }
51
+ if len(ids) != 2 {
52
+ t.Fatalf("ids=%v", ids)
53
+ }
54
+ }
55
+
56
+ func TestCheckPathDisjointRejectsDevritesPaths(t *testing.T) {
57
+ t.Parallel()
58
+ _, err := CheckPathDisjoint([]SlicePaths{
59
+ {ID: "a", Paths: []string{".devrites/work/state.md"}},
60
+ {ID: "b", Paths: []string{"src/b.go"}},
61
+ }, "")
62
+ if err == nil || !strings.Contains(err.Error(), ".devrites") {
63
+ t.Fatalf("expected .devrites rejection error, got %v", err)
64
+ }
65
+ }
66
+
67
+ func TestCheckPathDisjointSymlinkRoot(t *testing.T) {
68
+ t.Parallel()
69
+ dir := t.TempDir()
70
+ target := filepath.Join(dir, "real.go")
71
+ if err := os.WriteFile(target, []byte("x"), 0o644); err != nil {
72
+ t.Fatal(err)
73
+ }
74
+ link := filepath.Join(dir, "link.go")
75
+ if err := os.Symlink(target, link); err != nil {
76
+ t.Skip("symlinks unavailable")
77
+ }
78
+ _, err := CheckPathDisjoint([]SlicePaths{
79
+ {ID: "a", Paths: []string{"link.go"}},
80
+ {ID: "b", Paths: []string{"other.go"}},
81
+ }, dir)
82
+ if err == nil || !strings.Contains(err.Error(), "symlink") {
83
+ t.Fatalf("expected symlink error, got %v", err)
84
+ }
85
+ }
86
+
87
+ func TestParseSlicesJSONShapes(t *testing.T) {
88
+ t.Parallel()
89
+ a, err := ParseSlicesJSON([]byte(`{"slices":[{"id":"a","paths":["x.go"]},{"id":"b","paths":["y.go"]}]}`))
90
+ if err != nil || len(a) != 2 {
91
+ t.Fatalf("object shape: %v %#v", err, a)
92
+ }
93
+ b, err := ParseSlicesJSON([]byte(`[{"id":"a","paths":["x.go"]},{"id":"b","paths":["y.go"]}]`))
94
+ if err != nil || len(b) != 2 {
95
+ t.Fatalf("array shape: %v %#v", err, b)
96
+ }
97
+ }
@@ -7,14 +7,17 @@ import (
7
7
 
8
8
  // Report is the computed completeness status of a feature at its current phase.
9
9
  type Report struct {
10
- Slug string
11
- Phase Phase
12
- Required map[Section]bool
13
- Missing []Section // required-but-empty sections, in policy order
14
- RequiredFiles map[string]bool
15
- MissingFiles []string // required-but-empty workspace files, in lifecycle order
16
- present map[Section]bool
17
- diagnostics []ArtifactDiagnostic
10
+ Slug string
11
+ Phase Phase
12
+ Status string
13
+ NextAction string
14
+ PrinciplesPresent bool
15
+ Required map[Section]bool
16
+ Missing []Section // required-but-empty sections, in policy order
17
+ RequiredFiles map[string]bool
18
+ MissingFiles []string // required-but-empty workspace files, in lifecycle order
19
+ present map[Section]bool
20
+ diagnostics []ArtifactDiagnostic
18
21
  }
19
22
 
20
23
  // Status computes phase-relative completeness from one retained workspace observation.
@@ -35,6 +38,24 @@ func statusWithCallback(root, slug string, callback observationCallback) (*Repor
35
38
  return newObservationReport(observation, phase, policy), nil
36
39
  }
37
40
 
41
+ func observationCursorFields(observation *WorkspaceObservation) (status, nextAction string, principlesPresent bool) {
42
+ if fact, ok := observation.Fact(".devrites/principles.md"); ok && fact.State() == ArtifactPresent {
43
+ principlesPresent = true
44
+ }
45
+ fact, ok := observation.Fact(LedgerFile)
46
+ if !ok || fact.State() != ArtifactPresent {
47
+ return "", "", principlesPresent
48
+ }
49
+ lines := strings.Split(string(fact.Bytes()), "\n")
50
+ if value, ok := CursorField(lines, CursorStatus); ok {
51
+ status = value
52
+ }
53
+ if value, ok := CursorField(lines, CursorNextAction); ok {
54
+ nextAction = value
55
+ }
56
+ return status, nextAction, principlesPresent
57
+ }
58
+
38
59
  func newObservationReport(observation *WorkspaceObservation, phase Phase, policy PhasePolicy) *Report {
39
60
  present := observationSectionPresence(observation)
40
61
  required := requiredSections(policy)
@@ -44,15 +65,19 @@ func newObservationReport(observation *WorkspaceObservation, phase Phase, policy
44
65
  for i, artifact := range missingArtifacts {
45
66
  missingFiles[i] = string(artifact)
46
67
  }
68
+ status, nextAction, principlesPresent := observationCursorFields(observation)
47
69
  return &Report{
48
- Slug: observation.Slug(),
49
- Phase: phase,
50
- Required: required,
51
- Missing: missingObservationSections(present, policy.RequiredSections),
52
- RequiredFiles: requiredFiles,
53
- MissingFiles: missingFiles,
54
- present: present,
55
- diagnostics: diagnostics,
70
+ Slug: observation.Slug(),
71
+ Phase: phase,
72
+ Status: status,
73
+ NextAction: nextAction,
74
+ PrinciplesPresent: principlesPresent,
75
+ Required: required,
76
+ Missing: missingObservationSections(present, policy.RequiredSections),
77
+ RequiredFiles: requiredFiles,
78
+ MissingFiles: missingFiles,
79
+ present: present,
80
+ diagnostics: diagnostics,
56
81
  }
57
82
  }
58
83
 
package/engine/main.go CHANGED
@@ -10,6 +10,7 @@ import (
10
10
  "github.com/devrites/devrites/internal/gate"
11
11
  "github.com/devrites/devrites/internal/install"
12
12
  "github.com/devrites/devrites/internal/lib"
13
+ "github.com/devrites/devrites/internal/parallel"
13
14
  "github.com/devrites/devrites/internal/state"
14
15
  "github.com/devrites/devrites/internal/version"
15
16
  )
@@ -24,9 +25,17 @@ Usage:
24
25
  devrites-engine check readiness <slug> Check required files and the stable Build-input binding
25
26
  devrites-engine check readiness --emit-binding <slug> Emit the stable Build-input binding for Vet
26
27
  devrites-engine check seal <slug> Recheck the Build-input binding, final files, and evidence freshness
28
+ devrites-engine check path-disjoint [--root <dir>] [<json-file>|-]
29
+ Verify slice path sets are pairwise disjoint
30
+ devrites-engine check task-graph <slug> Validate tasks.md slice dependency graph
31
+ devrites-engine check skill-trust <path> Scan one skill/agent Markdown for trust violations
32
+ devrites-engine observe summary <slug> Emit sanitized JSON workspace summary
33
+ devrites-engine parallel <subcommand> Deterministic parallel worktree lease/create/integrate/cleanup
27
34
  devrites-engine state resolve <qid> "<ans>" Resolve an open question and update state atomically
28
35
  devrites-engine state close <slug> Archive a shipped feature and clear ACTIVE
29
36
  devrites-engine secret-scan [--staged] [--stdin] [slug] Scan exact staged blobs, stdin, or touched files; HIGH blocks
37
+ devrites-engine open-visual <path-or-name> [--slug <slug>] [--no-open]
38
+ Resolve a visual HTML file, optionally open it locally, print agent paths
30
39
  devrites-engine version Print the engine binary's version
31
40
  Exit codes:
32
41
  0 ok / gate passed
@@ -71,11 +80,17 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
71
80
  case "uninstall":
72
81
  return install.Run(args[1:], stdout, stderr, install.ModeUninstall)
73
82
  case "check":
74
- return cmdCheck(args[1:], stdout, stderr)
83
+ return cmdCheck(root, args[1:], stdin, stdout, stderr)
84
+ case "parallel":
85
+ return parallel.Run("parallel", args[1:], stdin, stdout, stderr)
86
+ case "observe":
87
+ return cmdObserve(root, args[1:], stdout, stderr)
75
88
  case "state":
76
89
  return cmdState(root, args[1:], stdout, stderr)
77
90
  case "secret-scan":
78
91
  return lib.SecretScan(root, args[1:], stdin, stdout, stderr)
92
+ case "open-visual":
93
+ return lib.OpenVisual(root, args[1:], stdout, stderr)
79
94
  case "version", "--version":
80
95
  fmt.Fprintln(stdout, version.Version)
81
96
  return exitOK
@@ -88,9 +103,9 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
88
103
  }
89
104
  }
90
105
 
91
- func cmdCheck(args []string, stdout, stderr io.Writer) int {
106
+ func cmdCheck(root string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
92
107
  if len(args) == 0 {
93
- fmt.Fprintln(stderr, "usage: devrites-engine check <candidate|readiness|seal> ...")
108
+ fmt.Fprintln(stderr, "usage: devrites-engine check <candidate|readiness|seal|path-disjoint|task-graph|skill-trust> ...")
94
109
  return exitUsage
95
110
  }
96
111
  sub, rest := args[0], args[1:]
@@ -99,6 +114,12 @@ func cmdCheck(args []string, stdout, stderr io.Writer) int {
99
114
  return cmdCandidate(rest, stdout, stderr)
100
115
  case "readiness", "seal":
101
116
  return cmdGate(gate.Kind(sub), rest, stdout, stderr)
117
+ case "path-disjoint":
118
+ return parallel.Run("path-disjoint", rest, stdin, stdout, stderr)
119
+ case "task-graph":
120
+ return cmdTaskGraph(root, rest, stdout, stderr)
121
+ case "skill-trust":
122
+ return cmdSkillTrust(rest, stdout, stderr)
102
123
  default:
103
124
  fmt.Fprintf(stderr, "devrites: unknown check %q\n", sub)
104
125
  return exitUsage
@@ -185,3 +206,55 @@ func cmdGate(kind gate.Kind, args []string, stdout, stderr io.Writer) int {
185
206
  fmt.Fprint(stdout, result.Render())
186
207
  return exitOK
187
208
  }
209
+
210
+ func cmdTaskGraph(root string, args []string, stdout, stderr io.Writer) int {
211
+ if len(args) != 1 {
212
+ fmt.Fprintln(stderr, "usage: devrites-engine check task-graph <slug>")
213
+ return exitUsage
214
+ }
215
+ if root == "" {
216
+ var err error
217
+ root, err = state.ResolveRoot(os.Getenv("DEVRITES_ROOT"))
218
+ if err != nil {
219
+ fmt.Fprintf(stderr, "devrites: %v\n", err)
220
+ return exitUsage
221
+ }
222
+ }
223
+ return lib.RunTaskGraphCheck(root, args[0], stdout, stderr)
224
+ }
225
+
226
+ func cmdSkillTrust(args []string, stdout, stderr io.Writer) int {
227
+ if len(args) != 1 {
228
+ fmt.Fprintln(stderr, "usage: devrites-engine check skill-trust <path>")
229
+ return exitUsage
230
+ }
231
+ return lib.RunSkillTrustCheck(args[0], stdout, stderr)
232
+ }
233
+
234
+ func cmdObserve(root string, args []string, stdout, stderr io.Writer) int {
235
+ if len(args) == 0 {
236
+ fmt.Fprintln(stderr, "usage: devrites-engine observe summary <slug>")
237
+ return exitUsage
238
+ }
239
+ if args[0] != "summary" {
240
+ fmt.Fprintf(stderr, "devrites: unknown observe command %q\n", args[0])
241
+ return exitUsage
242
+ }
243
+ if root == "" {
244
+ var err error
245
+ root, err = state.ResolveRoot(os.Getenv("DEVRITES_ROOT"))
246
+ if err != nil {
247
+ fmt.Fprintf(stderr, "devrites: %v\n", err)
248
+ return exitUsage
249
+ }
250
+ }
251
+ slug, code, err := lib.ActiveSlug(root, args[1:])
252
+ if err != nil {
253
+ fmt.Fprintf(stderr, "observe: %v\n", err)
254
+ if code == 0 {
255
+ code = exitUsage
256
+ }
257
+ return code
258
+ }
259
+ return lib.RunObserveSummary(root, slug, stdout, stderr)
260
+ }