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.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/docs/engine/commands.md +4 -0
- package/docs/orchestration.md +6 -3
- package/engine/internal/lib/cli_observe.go +66 -0
- package/engine/internal/lib/observe_summary.go +79 -0
- package/engine/internal/lib/skilltrust.go +196 -0
- package/engine/internal/lib/taskgraph.go +162 -0
- package/engine/internal/lib/taskgraph_test.go +150 -0
- package/engine/internal/lib/workspace_read.go +29 -0
- package/engine/internal/parallel/cli.go +467 -0
- package/engine/internal/parallel/git.go +181 -0
- package/engine/internal/parallel/lease.go +244 -0
- package/engine/internal/parallel/ops.go +416 -0
- package/engine/internal/parallel/parallel_test.go +242 -0
- package/engine/internal/parallel/pathdisjoint.go +129 -0
- package/engine/internal/parallel/pathdisjoint_test.go +97 -0
- package/engine/internal/state/status.go +41 -16
- package/engine/main.go +72 -3
- package/engine/root_routing_test.go +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/.claude/skills/rite-build/SKILL.md +29 -36
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/.claude/skills/rite-build/reference/output.md +2 -0
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
- package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
- package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/claude/skills/rite-build/SKILL.md +29 -36
- package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
- package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
- package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/codex/skills/rite-build/SKILL.md +29 -36
- package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
- package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
- 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
|
|
11
|
-
Phase
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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:
|
|
49
|
-
Phase:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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.
|
|
13
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
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
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
- Never rerun an unchanged check
|
|
46
|
-
- Unplanned dependency
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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.**
|
|
45
|
-
built is not charged again after retry, resume, or
|
|
46
|
-
saved cursor; if it is zero, report the cap and stop
|
|
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
|
|
59
|
-
|
|
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**
|
|
65
|
-
|
|
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)
|