devrites 4.4.0 → 4.4.2
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 +19 -494
- package/README.md +4 -4
- package/docs/cli.md +7 -7
- package/docs/command-map.md +1 -1
- package/docs/engine/commands.md +8 -7
- package/docs/engine/workspace-schema.md +5 -5
- package/engine/internal/gate/gate.go +9 -0
- package/engine/internal/gate/gate_test.go +110 -0
- package/engine/internal/gate/readiness_binding.go +9 -0
- package/engine/internal/gate/readiness_binding_test.go +2 -2
- package/engine/internal/lib/observe_summary.go +22 -22
- package/engine/internal/lib/taskgraph.go +4 -143
- package/engine/internal/lib/taskgraph_test.go +203 -0
- package/engine/internal/state/taskgraph.go +213 -0
- package/engine/internal/testutil/testutil.go +3 -0
- package/engine/main.go +2 -2
- package/engine/tests/adr_0027_readiness_binding_test.go +1 -1
- package/engine/tests/gate_test.go +4 -2
- package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +4 -1
- package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +4 -1
- package/pack/generated/codex/skills/rite/SKILL.md +1 -2
- package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +4 -1
- package/package.json +1 -1
- package/scripts/codex-generate.sh +2 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
package state
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"regexp"
|
|
6
|
+
"strings"
|
|
7
|
+
"unicode"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TaskSlice is one SLICE-### block parsed from tasks.md.
|
|
11
|
+
type TaskSlice struct {
|
|
12
|
+
ID string
|
|
13
|
+
Dependencies []string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// TaskGraphResult is the deterministic outcome of parsing a tasks.md slice graph.
|
|
17
|
+
type TaskGraphResult struct {
|
|
18
|
+
Slices []TaskSlice
|
|
19
|
+
Cycle []string // empty when acyclic
|
|
20
|
+
Unknown []string // dependency ids with no defining slice
|
|
21
|
+
Problems []string // human-readable blockers
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var (
|
|
25
|
+
sliceHeaderRE = regexp.MustCompile(`(?m)^##\s+(SLICE-\d+)\b`)
|
|
26
|
+
dependenciesRE = regexp.MustCompile(`(?m)^Dependencies:\s*(.+)\s*$`)
|
|
27
|
+
dependsOnRE = regexp.MustCompile(`(?m)^depends_on:\s*(.+)\s*$`)
|
|
28
|
+
sliceIDValidRE = regexp.MustCompile(`^SLICE-\d+$`)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// ParseTaskGraph reads tasks.md content and validates the slice dependency DAG.
|
|
32
|
+
func ParseTaskGraph(tasksMarkdown []byte) TaskGraphResult {
|
|
33
|
+
text := string(tasksMarkdown)
|
|
34
|
+
result := TaskGraphResult{}
|
|
35
|
+
if strings.TrimSpace(text) == "" {
|
|
36
|
+
result.Problems = append(result.Problems, "tasks.md is empty")
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
headers := sliceHeaderRE.FindAllStringSubmatchIndex(text, -1)
|
|
41
|
+
if len(headers) == 0 {
|
|
42
|
+
result.Problems = append(result.Problems, "no SLICE-### sections found")
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
known := make(map[string]bool, len(headers))
|
|
47
|
+
firstIndex := make(map[string]int, len(headers))
|
|
48
|
+
for i, match := range headers {
|
|
49
|
+
id := text[match[2]:match[3]]
|
|
50
|
+
if _, seen := firstIndex[id]; seen {
|
|
51
|
+
result.Problems = append(result.Problems, fmt.Sprintf("duplicate slice id %s", id))
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
firstIndex[id] = i
|
|
55
|
+
known[id] = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for i, match := range headers {
|
|
59
|
+
id := text[match[2]:match[3]]
|
|
60
|
+
if firstIndex[id] != i {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
start := match[1]
|
|
64
|
+
end := len(text)
|
|
65
|
+
if i+1 < len(headers) {
|
|
66
|
+
end = headers[i+1][0]
|
|
67
|
+
}
|
|
68
|
+
block := text[start:end]
|
|
69
|
+
deps, problems := parseSliceGraph(id, block)
|
|
70
|
+
result.Problems = append(result.Problems, problems...)
|
|
71
|
+
result.Slices = append(result.Slices, TaskSlice{ID: id, Dependencies: deps})
|
|
72
|
+
for _, dep := range deps {
|
|
73
|
+
if !known[dep] {
|
|
74
|
+
result.Unknown = append(result.Unknown, dep)
|
|
75
|
+
result.Problems = append(result.Problems, fmt.Sprintf("%s depends on unknown slice %s", id, dep))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if cycle := findTaskCycle(result.Slices); len(cycle) > 0 {
|
|
81
|
+
result.Cycle = cycle
|
|
82
|
+
result.Problems = append(result.Problems, "dependency cycle: "+strings.Join(cycle, " -> "))
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func parseSliceGraph(id, block string) ([]string, []string) {
|
|
88
|
+
var problems []string
|
|
89
|
+
depsLine := firstLineValue(dependenciesRE, block)
|
|
90
|
+
mirrorLine := firstLineValue(dependsOnRE, block)
|
|
91
|
+
deps, malformed := parseIDList(depsLine)
|
|
92
|
+
for _, token := range malformed {
|
|
93
|
+
problems = append(problems, fmt.Sprintf("%s has malformed dependency %q", id, token))
|
|
94
|
+
}
|
|
95
|
+
mirror, mirrorMalformed := parseIDList(mirrorLine)
|
|
96
|
+
for _, token := range mirrorMalformed {
|
|
97
|
+
problems = append(problems, fmt.Sprintf("%s has malformed depends_on %q", id, token))
|
|
98
|
+
}
|
|
99
|
+
if depsLine != "" && mirrorLine != "" && !sameIDSet(deps, mirror) {
|
|
100
|
+
problems = append(problems, fmt.Sprintf("%s Dependencies and depends_on sets differ", id))
|
|
101
|
+
}
|
|
102
|
+
if depsLine == "" && mirrorLine == "" {
|
|
103
|
+
problems = append(problems, fmt.Sprintf("%s is missing Dependencies", id))
|
|
104
|
+
}
|
|
105
|
+
if depsLine == "" {
|
|
106
|
+
deps = mirror
|
|
107
|
+
}
|
|
108
|
+
return deps, problems
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func firstLineValue(re *regexp.Regexp, block string) string {
|
|
112
|
+
match := re.FindStringSubmatch(block)
|
|
113
|
+
if len(match) < 2 {
|
|
114
|
+
return ""
|
|
115
|
+
}
|
|
116
|
+
return strings.TrimSpace(match[1])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func parseIDList(raw string) ([]string, []string) {
|
|
120
|
+
raw = strings.TrimSpace(raw)
|
|
121
|
+
raw = strings.Trim(raw, "[]")
|
|
122
|
+
raw = strings.TrimSpace(raw)
|
|
123
|
+
if raw == "" || strings.EqualFold(raw, "none") {
|
|
124
|
+
return nil, nil
|
|
125
|
+
}
|
|
126
|
+
parts := strings.FieldsFunc(raw, func(r rune) bool {
|
|
127
|
+
return r == ',' || r == ';' || unicode.IsSpace(r)
|
|
128
|
+
})
|
|
129
|
+
var ids []string
|
|
130
|
+
var malformed []string
|
|
131
|
+
seen := make(map[string]bool)
|
|
132
|
+
for _, part := range parts {
|
|
133
|
+
id := strings.Trim(strings.TrimSpace(part), "`'\"(){}")
|
|
134
|
+
if id == "" {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
if !sliceIDValidRE.MatchString(id) {
|
|
138
|
+
malformed = append(malformed, id)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if seen[id] {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
seen[id] = true
|
|
145
|
+
ids = append(ids, id)
|
|
146
|
+
}
|
|
147
|
+
return ids, malformed
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func sameIDSet(a, b []string) bool {
|
|
151
|
+
if len(a) != len(b) {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
seen := make(map[string]bool, len(a))
|
|
155
|
+
for _, id := range a {
|
|
156
|
+
seen[id] = true
|
|
157
|
+
}
|
|
158
|
+
for _, id := range b {
|
|
159
|
+
if !seen[id] {
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func findTaskCycle(slices []TaskSlice) []string {
|
|
167
|
+
graph := make(map[string][]string, len(slices))
|
|
168
|
+
for _, slice := range slices {
|
|
169
|
+
graph[slice.ID] = append([]string(nil), slice.Dependencies...)
|
|
170
|
+
}
|
|
171
|
+
visited := make(map[string]uint8, len(slices))
|
|
172
|
+
stack := make([]string, 0, len(slices))
|
|
173
|
+
var cycle []string
|
|
174
|
+
|
|
175
|
+
var visit func(id string) bool
|
|
176
|
+
visit = func(id string) bool {
|
|
177
|
+
switch visited[id] {
|
|
178
|
+
case 2:
|
|
179
|
+
return false
|
|
180
|
+
case 1:
|
|
181
|
+
for i := len(stack) - 1; i >= 0; i-- {
|
|
182
|
+
cycle = append(cycle, stack[i])
|
|
183
|
+
if stack[i] == id {
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for i, j := 0, len(cycle)-1; i < j; i, j = i+1, j-1 {
|
|
188
|
+
cycle[i], cycle[j] = cycle[j], cycle[i]
|
|
189
|
+
}
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
visited[id] = 1
|
|
193
|
+
stack = append(stack, id)
|
|
194
|
+
for _, dep := range graph[id] {
|
|
195
|
+
if visit(dep) {
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
stack = stack[:len(stack)-1]
|
|
200
|
+
visited[id] = 2
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for _, slice := range slices {
|
|
205
|
+
if visited[slice.ID] == 0 {
|
|
206
|
+
cycle = nil
|
|
207
|
+
if visit(slice.ID) {
|
|
208
|
+
return cycle
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return nil
|
|
213
|
+
}
|
|
@@ -7,6 +7,9 @@ import (
|
|
|
7
7
|
"testing"
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
+
// CanonicalTasksMarkdown is a minimal valid slice graph for gate fixtures.
|
|
11
|
+
const CanonicalTasksMarkdown = "# Tasks\n\n## SLICE-001 Ready\nDependencies: none\n"
|
|
12
|
+
|
|
10
13
|
// CopyTree recursively copies the directory tree at src into dst, creating dst
|
|
11
14
|
// and any parents. It is used to give each test an isolated, writable copy of a
|
|
12
15
|
// read-only fixture.
|
package/engine/main.go
CHANGED
|
@@ -22,9 +22,9 @@ Usage:
|
|
|
22
22
|
devrites-engine update [flags] Update an existing DevRites install in place
|
|
23
23
|
devrites-engine uninstall [flags] Remove a DevRites install, preserving runtime state
|
|
24
24
|
devrites-engine check candidate <slug> Validate and hash the closed project candidate
|
|
25
|
-
devrites-engine check readiness <slug> Check required files
|
|
25
|
+
devrites-engine check readiness <slug> Check required files, tasks.md graph, and Build-input binding
|
|
26
26
|
devrites-engine check readiness --emit-binding <slug> Emit the stable Build-input binding for Vet
|
|
27
|
-
devrites-engine check seal <slug> Recheck
|
|
27
|
+
devrites-engine check seal <slug> Recheck files, tasks.md graph, Build-input binding, and evidence freshness
|
|
28
28
|
devrites-engine check path-disjoint [--root <dir>] [<json-file>|-]
|
|
29
29
|
Verify slice path sets are pairwise disjoint
|
|
30
30
|
devrites-engine check task-graph <slug> Validate tasks.md slice dependency graph
|
|
@@ -23,7 +23,7 @@ func TestADR0027ReadinessBindingBlocksPlanDriftWithRestoredMtime(t *testing.T) {
|
|
|
23
23
|
"decision-coverage.md": "# Decision coverage\n\nCLEAR\n",
|
|
24
24
|
"architecture.md": "# Architecture\n\nUse the deterministic gate.\n",
|
|
25
25
|
"plan.md": "# Plan\n\nBuild slice A.\n",
|
|
26
|
-
"tasks.md":
|
|
26
|
+
"tasks.md": testutil.CanonicalTasksMarkdown,
|
|
27
27
|
"traceability.md": "# Traceability\n\nAC-001 -> slice A.\n",
|
|
28
28
|
"eng-review.md": "# Engineering review\n\nREADY\n",
|
|
29
29
|
"test-plan.md": "# Test plan\n\nRun focused Go tests.\n",
|
|
@@ -70,7 +70,7 @@ func TestReadinessEmitBindingPassesExactCLIContract(t *testing.T) {
|
|
|
70
70
|
writeCompleteGateCLIWorkspace(t, root, slug, state.PhaseBuild, state.PhaseBuild, "none\n")
|
|
71
71
|
|
|
72
72
|
out, errOut, code := runDevrites(t, root, "check", "readiness", "--emit-binding", slug)
|
|
73
|
-
const wantOut = "Readiness inputs SHA-256:
|
|
73
|
+
const wantOut = "Readiness inputs SHA-256: f72109687057f33d5d7f05e5436a4dcc88ccf87367d0933afd9fdcf1a023f5c0\n"
|
|
74
74
|
if code != 0 || out != wantOut || errOut != "" {
|
|
75
75
|
t.Fatalf("code=%d stdout=%q stderr=%q, want code=0 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
76
76
|
}
|
|
@@ -565,6 +565,8 @@ func writeCompleteGateCLIWorkspace(t *testing.T, root, slug string, current, req
|
|
|
565
565
|
case "questions.md":
|
|
566
566
|
questionsRequired = true
|
|
567
567
|
content = questions
|
|
568
|
+
case "tasks.md":
|
|
569
|
+
content = testutil.CanonicalTasksMarkdown
|
|
568
570
|
}
|
|
569
571
|
testutil.WriteFile(t, filepath.Join(root, "work", slug, name), content)
|
|
570
572
|
}
|
|
@@ -590,7 +592,7 @@ func newFinalSealRepo(t *testing.T) (string, string) {
|
|
|
590
592
|
"decision-coverage.md": "# Decision coverage\n\nCLEAR\n",
|
|
591
593
|
"architecture.md": "# Architecture\n\nUse existing gates.\n",
|
|
592
594
|
"plan.md": "# Plan\n\nCompose the final checks.\n",
|
|
593
|
-
"tasks.md":
|
|
595
|
+
"tasks.md": testutil.CanonicalTasksMarkdown,
|
|
594
596
|
"traceability.md": "# Traceability\n\nAC-001 -> final seal.\n",
|
|
595
597
|
"eng-review.md": "# Engineering review\n\nPASS\n",
|
|
596
598
|
"test-plan.md": "# Test plan\n\nRun focused Go tests.\n",
|
|
@@ -37,7 +37,10 @@ define the contract first (so both sides can proceed) and trigger `devrites-doub
|
|
|
37
37
|
before standing the interface.
|
|
38
38
|
|
|
39
39
|
After editing `tasks.md`, run `devrites-engine check task-graph <slug>` before Vet.
|
|
40
|
-
|
|
40
|
+
`check readiness` and `check seal` also reject cycles, unknown dependencies,
|
|
41
|
+
malformed tokens, duplicate slice IDs, a missing `Dependencies`/`depends_on`
|
|
42
|
+
line, and a `depends_on` set that disagrees with `Dependencies`. Cycles or
|
|
43
|
+
unknown dependencies block readiness.
|
|
41
44
|
|
|
42
45
|
For monorepos/multiple repositories, annotate the proven root and deployable on each node.
|
|
43
46
|
For data/integration changes, include recovery ordering: expand before new writers,
|
|
@@ -37,7 +37,10 @@ define the contract first (so both sides can proceed) and trigger `devrites-doub
|
|
|
37
37
|
before standing the interface.
|
|
38
38
|
|
|
39
39
|
After editing `tasks.md`, run `devrites-engine check task-graph <slug>` before Vet.
|
|
40
|
-
|
|
40
|
+
`check readiness` and `check seal` also reject cycles, unknown dependencies,
|
|
41
|
+
malformed tokens, duplicate slice IDs, a missing `Dependencies`/`depends_on`
|
|
42
|
+
line, and a `depends_on` set that disagrees with `Dependencies`. Cycles or
|
|
43
|
+
unknown dependencies block readiness.
|
|
41
44
|
|
|
42
45
|
For monorepos/multiple repositories, annotate the proven root and deployable on each node.
|
|
43
46
|
For data/integration changes, include recovery ordering: expand before new writers,
|
|
@@ -19,12 +19,11 @@ When they ask how phases connect, load [`reference/menu.md`](reference/menu.md).
|
|
|
19
19
|
|
|
20
20
|
## Dispatch
|
|
21
21
|
|
|
22
|
-
If `$ARGUMENTS` starts with a verb in this table, **load the matching skill and execute its workflow** with the remainder of `$ARGUMENTS` as that skill's argument.
|
|
22
|
+
If `$ARGUMENTS` starts with a verb in this table, **load the matching skill and execute its workflow** with the remainder of `$ARGUMENTS` as that skill's argument. Resolve the installed skill path:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
V=<verb>; ARGS="<remaining args>"
|
|
26
26
|
F=.agents/skills/rite-$V/SKILL.md
|
|
27
|
-
[ -f "$F" ] || F=.agents/skills/rite-$V/SKILL.md
|
|
28
27
|
# Then Read "$F" and follow its workflow with $ARGS as that skill's $ARGUMENTS.
|
|
29
28
|
```
|
|
30
29
|
|
|
@@ -37,7 +37,10 @@ define the contract first (so both sides can proceed) and trigger `devrites-doub
|
|
|
37
37
|
before standing the interface.
|
|
38
38
|
|
|
39
39
|
After editing `tasks.md`, run `devrites-engine check task-graph <slug>` before Vet.
|
|
40
|
-
|
|
40
|
+
`check readiness` and `check seal` also reject cycles, unknown dependencies,
|
|
41
|
+
malformed tokens, duplicate slice IDs, a missing `Dependencies`/`depends_on`
|
|
42
|
+
line, and a `depends_on` set that disagrees with `Dependencies`. Cycles or
|
|
43
|
+
unknown dependencies block readiness.
|
|
41
44
|
|
|
42
45
|
For monorepos/multiple repositories, annotate the proven root and deployable on each node.
|
|
43
46
|
For data/integration changes, include recovery ordering: expand before new writers,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.2",
|
|
4
4
|
"description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code and Codex",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|
|
@@ -8,6 +8,8 @@ gen_codex_markdown_file() {
|
|
|
8
8
|
local _src="$1" _out="$2"
|
|
9
9
|
mkdir -p "$(dirname "$_out")"
|
|
10
10
|
sed -E \
|
|
11
|
+
-e 's#Try post-install path first, fall back to pre-install:#Resolve the installed skill path:#g' \
|
|
12
|
+
-e '/^\[ -f "\$F" \] \|\| F=[^ ].*SKILL\.md$/d' \
|
|
11
13
|
-e 's#(pack/)?\.claude/agents/devrites-\{security-auditor,performance-reviewer,simplifier-reviewer\}\.md#.codex/agents/devrites-security-auditor.toml`, `.codex/agents/devrites-performance-reviewer.toml`, or `.codex/agents/devrites-simplifier-reviewer.toml#g' \
|
|
12
14
|
-e 's#(\.\./)+agents/devrites-\{security-auditor,performance-reviewer,simplifier-reviewer\}\.md#.codex/agents/devrites-security-auditor.toml`, `.codex/agents/devrites-performance-reviewer.toml`, or `.codex/agents/devrites-simplifier-reviewer.toml#g' \
|
|
13
15
|
-e 's#pack/\.claude/skills/devrites-lib/scripts/#.agents/skills/devrites-lib/scripts/#g' \
|