devrites 4.2.0 → 4.3.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 (60) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/docs/engine/commands.md +4 -0
  4. package/docs/orchestration.md +6 -3
  5. package/engine/internal/lib/cli_observe.go +66 -0
  6. package/engine/internal/lib/observe_summary.go +79 -0
  7. package/engine/internal/lib/skilltrust.go +196 -0
  8. package/engine/internal/lib/taskgraph.go +162 -0
  9. package/engine/internal/lib/taskgraph_test.go +150 -0
  10. package/engine/internal/lib/workspace_read.go +29 -0
  11. package/engine/internal/parallel/cli.go +467 -0
  12. package/engine/internal/parallel/git.go +181 -0
  13. package/engine/internal/parallel/lease.go +244 -0
  14. package/engine/internal/parallel/ops.go +416 -0
  15. package/engine/internal/parallel/parallel_test.go +242 -0
  16. package/engine/internal/parallel/pathdisjoint.go +129 -0
  17. package/engine/internal/parallel/pathdisjoint_test.go +97 -0
  18. package/engine/internal/state/status.go +41 -16
  19. package/engine/main.go +72 -3
  20. package/engine/root_routing_test.go +1 -1
  21. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  22. package/pack/.claude/skills/rite-build/SKILL.md +29 -36
  23. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
  24. package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  25. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  26. package/pack/.claude/skills/rite-build/reference/output.md +2 -0
  27. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
  28. package/pack/.claude/skills/rite-build/reference/phase-contract.md +6 -5
  29. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  30. package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  31. package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
  32. package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
  33. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  34. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  35. package/pack/generated/claude/skills/rite-build/SKILL.md +29 -36
  36. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
  37. package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  38. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  39. package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
  40. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
  41. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +6 -5
  42. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  43. package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  44. package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
  45. package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
  46. package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  47. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  48. package/pack/generated/codex/skills/rite-build/SKILL.md +29 -36
  49. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
  50. package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  51. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
  52. package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
  53. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
  54. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +6 -5
  55. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
  56. package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
  57. package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
  58. package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
  59. package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
  60. package/package.json +1 -1
@@ -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,6 +25,12 @@ 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
@@ -71,7 +78,11 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
71
78
  case "uninstall":
72
79
  return install.Run(args[1:], stdout, stderr, install.ModeUninstall)
73
80
  case "check":
74
- return cmdCheck(args[1:], stdout, stderr)
81
+ return cmdCheck(root, args[1:], stdin, stdout, stderr)
82
+ case "parallel":
83
+ return parallel.Run("parallel", args[1:], stdin, stdout, stderr)
84
+ case "observe":
85
+ return cmdObserve(root, args[1:], stdout, stderr)
75
86
  case "state":
76
87
  return cmdState(root, args[1:], stdout, stderr)
77
88
  case "secret-scan":
@@ -88,9 +99,9 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
88
99
  }
89
100
  }
90
101
 
91
- func cmdCheck(args []string, stdout, stderr io.Writer) int {
102
+ func cmdCheck(root string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
92
103
  if len(args) == 0 {
93
- fmt.Fprintln(stderr, "usage: devrites-engine check <candidate|readiness|seal> ...")
104
+ fmt.Fprintln(stderr, "usage: devrites-engine check <candidate|readiness|seal|path-disjoint|task-graph|skill-trust> ...")
94
105
  return exitUsage
95
106
  }
96
107
  sub, rest := args[0], args[1:]
@@ -99,6 +110,12 @@ func cmdCheck(args []string, stdout, stderr io.Writer) int {
99
110
  return cmdCandidate(rest, stdout, stderr)
100
111
  case "readiness", "seal":
101
112
  return cmdGate(gate.Kind(sub), rest, stdout, stderr)
113
+ case "path-disjoint":
114
+ return parallel.Run("path-disjoint", rest, stdin, stdout, stderr)
115
+ case "task-graph":
116
+ return cmdTaskGraph(root, rest, stdout, stderr)
117
+ case "skill-trust":
118
+ return cmdSkillTrust(rest, stdout, stderr)
102
119
  default:
103
120
  fmt.Fprintf(stderr, "devrites: unknown check %q\n", sub)
104
121
  return exitUsage
@@ -185,3 +202,55 @@ func cmdGate(kind gate.Kind, args []string, stdout, stderr io.Writer) int {
185
202
  fmt.Fprint(stdout, result.Render())
186
203
  return exitOK
187
204
  }
205
+
206
+ func cmdTaskGraph(root string, args []string, stdout, stderr io.Writer) int {
207
+ if len(args) != 1 {
208
+ fmt.Fprintln(stderr, "usage: devrites-engine check task-graph <slug>")
209
+ return exitUsage
210
+ }
211
+ if root == "" {
212
+ var err error
213
+ root, err = state.ResolveRoot(os.Getenv("DEVRITES_ROOT"))
214
+ if err != nil {
215
+ fmt.Fprintf(stderr, "devrites: %v\n", err)
216
+ return exitUsage
217
+ }
218
+ }
219
+ return lib.RunTaskGraphCheck(root, args[0], stdout, stderr)
220
+ }
221
+
222
+ func cmdSkillTrust(args []string, stdout, stderr io.Writer) int {
223
+ if len(args) != 1 {
224
+ fmt.Fprintln(stderr, "usage: devrites-engine check skill-trust <path>")
225
+ return exitUsage
226
+ }
227
+ return lib.RunSkillTrustCheck(args[0], stdout, stderr)
228
+ }
229
+
230
+ func cmdObserve(root string, args []string, stdout, stderr io.Writer) int {
231
+ if len(args) == 0 {
232
+ fmt.Fprintln(stderr, "usage: devrites-engine observe summary <slug>")
233
+ return exitUsage
234
+ }
235
+ if args[0] != "summary" {
236
+ fmt.Fprintf(stderr, "devrites: unknown observe command %q\n", args[0])
237
+ return exitUsage
238
+ }
239
+ if root == "" {
240
+ var err error
241
+ root, err = state.ResolveRoot(os.Getenv("DEVRITES_ROOT"))
242
+ if err != nil {
243
+ fmt.Fprintf(stderr, "devrites: %v\n", err)
244
+ return exitUsage
245
+ }
246
+ }
247
+ slug, code, err := lib.ActiveSlug(root, args[1:])
248
+ if err != nil {
249
+ fmt.Fprintf(stderr, "observe: %v\n", err)
250
+ if code == 0 {
251
+ code = exitUsage
252
+ }
253
+ return code
254
+ }
255
+ return lib.RunObserveSummary(root, slug, stdout, stderr)
256
+ }
@@ -126,7 +126,7 @@ func TestNestedCommandFamilyUsageListsOnlyRetainedCommands(t *testing.T) {
126
126
  want string
127
127
  removed []string
128
128
  }{
129
- {args: []string{"check"}, want: "check <candidate|readiness|seal>", removed: []string{"spec"}},
129
+ {args: []string{"check"}, want: "check <candidate|readiness|seal|path-disjoint|task-graph|skill-trust>", removed: []string{"spec"}},
130
130
  {args: []string{"state"}, want: "state <resolve|close>", removed: []string{"clarify", "tick-afk", "recovery"}},
131
131
  } {
132
132
  t.Run(test.args[0], func(t *testing.T) {
@@ -96,6 +96,28 @@ External sources are references, not authority. Promote only when one
96
96
 
97
97
  Missing field → no promotion.
98
98
 
99
+ ## Skill trust tiers
100
+
101
+ Every skill or agent surface belongs to exactly one trust tier. Higher tiers may
102
+ constrain lower ones; nothing may weaken shipped gates or permissions.
103
+
104
+ | Tier | Source | Authority | Install check |
105
+ | --- | --- | --- | --- |
106
+ | **shipped** | `pack/.claude/` built by CI | Full workflow authority | manifest hash + host parity |
107
+ | **project-local** | Repo-scoped customization approved by a human | May extend project rules; cannot weaken DevRites method | `devrites-engine check skill-trust` on the path |
108
+ | **imported** | External skill with `docs/research/` admission record | Read/adapt only after provenance review | skill-trust scan + admission record required |
109
+ | **untrusted** | Unknown origin or failed scan | Reference-only; never executable authority | block on any HIGH finding |
110
+
111
+ Before promoting or installing project-local/imported Markdown, run:
112
+
113
+ ```bash
114
+ devrites-engine check skill-trust <path>
115
+ ```
116
+
117
+ HIGH findings (prompt-injection override prose, suspicious Unicode, credential exfil
118
+ patterns, sensitive path references) block installation. MEDIUM findings require
119
+ explicit human acknowledgment in the customization diff, not silent merge.
120
+
99
121
  ## Match form to failure
100
122
 
101
123
  - Rule breaks under pressure → hard guard + rationalization rebuttal + stop list.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rite-build
3
- description: Build the next approved vertical slice with evidence. HITL stops after one; explicit AFK may chain bounded green slices.
4
- argument-hint: "[slice number or name]"
3
+ description: Build the next approved vertical slice with evidence. HITL one-slice default; AFK may chain serially; opt-in `--parallel N` (2≤N≤3) for path-disjoint worktrees.
4
+ argument-hint: "[--parallel N] [slice number or name]"
5
5
  user-invocable: true
6
6
  ---
7
7
 
@@ -9,8 +9,9 @@ user-invocable: true
9
9
 
10
10
  Build and prove one slice. HITL stops; a later user invocation starts the next.
11
11
  Explicit `.devrites/AFK` alone lets the controlling root chain pending slices
12
- under green proof, caps, and pause rules. Every wright returns after it. Read the
13
- active workspace first; without one, route `/rite-spec <feature>`.
12
+ serially under green proof, caps, and pause rules. Every wright returns after it.
13
+ **Opt-in:** `/rite-build --parallel N` (2≤N≤3; N=1≡serial) follows
14
+ [`reference/parallel-batch.md`](reference/parallel-batch.md).
14
15
 
15
16
  Root owns gates/bookkeeping. Fresh
16
17
  [`devrites-slice-wright`](../../agents/devrites-slice-wright.md) writes product
@@ -21,45 +22,37 @@ Execute [`reference/phase-contract.md`](reference/phase-contract.md); dispatch u
21
22
 
22
23
  ## Required rules
23
24
 
24
- Read `.claude/skills/devrites-lib/reference/standards/core.md` first. Load only
25
- rules triggered by the slice:
26
-
27
- - coding style, error handling, testing, [`reference/tdd.md`](reference/tdd.md),
28
- patterns, and definition of done;
29
- - binding `.devrites/principles.md` invariants when present;
30
- - security for input/auth/data/integrations;
31
- - repository topology for multiple roots/languages or generated/vendor surfaces;
32
- - data integrity for durable state, migration, concurrency, tenancy, or retention;
33
- - integration reliability for API/webhook/queue/job/cache/service boundaries.
34
-
35
- The wright also applies the canonical anti-slop list. Root verifies its return;
36
- it never patches source itself.
25
+ Read `.claude/skills/devrites-lib/reference/standards/core.md` first. Load only triggered rules:
26
+ coding/error/testing/[`tdd.md`](reference/tdd.md)/patterns/DoD; binding
27
+ `.devrites/principles.md`; security; topology; data integrity; integration reliability.
28
+ Wright applies anti-slop; root verifies returns and never patches source.
37
29
 
38
30
  ## Invariants
39
31
 
40
- - One slice per wright dispatch; writers are serial. Use the native-worktree pilot
41
- only when `wright-dispatch.md`'s clean preflight and reconciliation both hold,
42
- otherwise serial same-worktree writing.
43
- - Exact feature scope only. Record adjacent issues and Things I didn't touch;
44
- reject any returned diff outside the task's explicit source/test paths.
45
- - Never rerun an unchanged check. Re-prove after edits.
46
- - Unplanned dependency, design system, objective technical gap, or repair stays
47
- inside Vet/Spec Drift Guard. Ask only for licensing/cost/security/product or
48
- explicit architecture-policy decisions.
49
- - Root never edits product source/tests. It writes canonical `.devrites/`
50
- bookkeeping and applies the Workflow Artifact route only. The supported host's
51
- wright is sole product writer. Put exact project-relative paths directly in the
52
- task; compare returned file list and `git diff --name-only`; extras hard-stop.
53
- - Project principles are binding. An unavoidable conflict or irreversible risk
54
- needs a human-approved scoped exception or stop, never silent balancing.
55
- - Evidence beats confidence. Never weaken a failing test, skip TDD, widen a
56
- writer, or self-approve a wright return. Route drift through
57
- [`reference/spec-drift-guard.md`](reference/spec-drift-guard.md); checkpoint
58
- mode follows [`reference/checkpoint.md`](reference/checkpoint.md).
32
+ - Default: one slice; writers serial on control. Parallel only via `--parallel N`
33
+ under [`reference/parallel-batch.md`](reference/parallel-batch.md). Same-worktree
34
+ multi-writer / root-emulated concurrency forbidden. Native-worktree pilot =
35
+ single-slice isolation when `wright-dispatch.md` preflight + reconcile hold.
36
+ - Exact feature scope only; reject out-of-allowlist diffs; record adjacent issues.
37
+ - Never rerun an unchanged check; re-prove after edits.
38
+ - Unplanned dependency/design-system/gap/repair Vet/Spec Drift Guard. Ask only
39
+ for licensing/cost/security/product or explicit architecture-policy decisions.
40
+ - Root never edits product source/tests (`.devrites/` + Workflow Artifact only).
41
+ Wright is sole product writer; extras in returned paths/`git diff --name-only` hard-stop.
42
+ - Principles bind; irreversible conflict needs human exception or stop.
43
+ - Evidence beats confidence. Never weaken tests, skip TDD, widen writers, or
44
+ self-approve. Drift [`spec-drift-guard.md`](reference/spec-drift-guard.md);
45
+ checkpoint [`checkpoint.md`](reference/checkpoint.md).
59
46
 
60
47
  ## Workflow Artifact branch
61
48
 
62
49
  <!-- workflow-artifact-adapter: {"module":"devrites-lib/reference/standards/workflow-artifacts.md","entry":"Vet-ready admitted bytes require root authorship outside product wright","action":"ROOT_TRANSACTION; root writes only admitted .devrites/** targets","return":"saved Build slice cursor; wright product allowlist unchanged"} -->
50
+ ## `--parallel N` (opt-in)
51
+
52
+ Omitted/`1` ≡ serial; `2`/`3` → path-disjoint fan-out when eligible; else hard refuse.
53
+ All-green serial integrate; one red/gap aborts. AFK charges after integrate only.
54
+ Running lease blocks another `/rite-build`. Details: `parallel-batch.md`.
55
+
63
56
  ## Execute and reply
64
57
 
65
58
  Run every step in `reference/phase-contract.md`: readiness, one target, dispatch
@@ -8,16 +8,8 @@ Load the shared
8
8
  contract for the sentinel schema, defaults, gate ceiling, and mutable-counter
9
9
  ownership. This file owns only Build's dispatch, charging, and red-path behavior.
10
10
 
11
- These rules follow established autonomous-coding loops, including Ralph Wiggum and
12
- Claude Code auto mode:
13
-
14
- 1. **Require green feedback.** Tests, types, and lint must pass before a slice is
15
- marked `built`.
16
- 2. **Cap iterations.** `max_slices` is the hard limit.
17
- 3. **Run gates before the action they control.** A post-action gate is only a review
18
- queue.
19
- 4. **Keep irreversible work manual.** Destructive work, auth boundaries, and public
20
- API breaks always pause regardless of the sentinel.
11
+ Rules: green before `built`; hard `max_slices` cap; gates before the action they
12
+ control; irreversible work (destructive/auth/public API) always pauses.
21
13
 
22
14
  ## Iteration cap
23
15
 
@@ -41,32 +33,33 @@ The controlling root owns the cap:
41
33
  A controlling orchestrator may pre-seed the remaining field from a validated
42
34
  post-plan budget before the first dispatch; never increase or reinitialize an
43
35
  existing value.
44
- 3. **Charge exactly once after each green built slice.** A slice already marked
45
- built is not charged again after retry, resume, or compaction. Re-read the
46
- saved cursor; if it is zero, report the cap and stop before the next
47
- dispatch.
36
+ 3. **Charge exactly once after each green built slice.** On the control tree, a slice
37
+ already marked built is not charged again after retry, resume, or
38
+ compaction. Re-read the saved cursor; if it is zero, report the cap and stop
39
+ before the next dispatch.
40
+ - **Serial:** charge when fail-on-red is green and the built record is written
41
+ (same rewrite as step 2).
42
+ - **Parallel `--parallel`:** charge only after **successful serial integrate**
43
+ — once per integrated green sibling. Abort / integrate-failed → charge **0**.
44
+ Do not charge on worktree-green before integrate. See
45
+ [`parallel-batch.md`](parallel-batch.md).
48
46
 
49
47
  Use this stop message:
50
48
 
51
- ```
49
+ ```text
52
50
  AFK cap reached. Raise `state.md` `AFK slices remaining` or remove the sentinel to continue.
53
51
  ```
54
52
 
55
53
  `max_slices` itself is read-only and never rewritten. No exit-code command
56
54
  enforces this policy.
57
55
 
58
- Choose a missing or large cap deliberately. Ralph's rule is 5-10 iterations for small
59
- tasks and 30-50 for larger ones. Do not use `unlimited` for work that has not completed
60
- successfully in HITL.
56
+ Choose caps deliberately (≈5–10 small, ≈30–50 larger). Avoid `unlimited` until HITL
57
+ has succeeded for the work.
61
58
 
62
59
  ## Fail-on-red
63
60
 
64
- The **fail-on-red step** (workflow step 5) refuses to mark a slice `built` if targeted
65
- tests, types, or lint are red:
66
-
67
- - A red signal means either the slice's contract is wrong or the implementation/proof path is.
68
- The slice cannot advance, but an objective root cause is agent-owned recovery work.
69
- - Marking it `built` would let the next slice build on broken state.
61
+ The **fail-on-red step** refuses `built` when targeted tests/types/lint are red. Red means
62
+ wrong contract or proof path — agent-owned recovery; never advance on broken state.
70
63
 
71
64
  The fail-on-red path:
72
65
 
@@ -105,7 +98,7 @@ The hook is a single shell command run on the `awaiting_human` transition. Envir
105
98
  the hook receives:
106
99
 
107
100
  | Var | Value |
108
- |---|---|
101
+ | --- | --- |
109
102
  | `DEVRITES_QID` | the new qid (e.g. `q-2026-05-28-001`) |
110
103
  | `DEVRITES_GATE` | `advisory` / `validating` / `blocking` / `escalating` |
111
104
  | `DEVRITES_SLICE` | `<N — name>` |
@@ -117,6 +110,7 @@ The hook is best effort: a non-zero exit does **not** roll back the pause. Failu
117
110
  logged to `evidence.md` so the user sees them on return.
118
111
 
119
112
  Example targets:
113
+
120
114
  - `curl -d "$DEVRITES_QID: $DEVRITES_QUESTION" ntfy.sh/my-topic`
121
115
  - `osascript -e "display notification \"$DEVRITES_QUESTION\" with title \"DevRites: $DEVRITES_GATE\""`
122
116
  - `pb push "$DEVRITES_SLUG: $DEVRITES_QUESTION"` (via pushbullet CLI)