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.
@@ -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 and the stable Build-input binding
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 the Build-input binding, final files, and evidence freshness
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": "# Tasks\n\n- [ ] Build slice A.\n",
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: 71c2d192ea09bca1d2c8806cb197e7fa4d1d08e0b331cf25b2e1bcb7ecac34e4\n"
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": "# Tasks\n\n- [x] Build final seal.\n",
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
- Cycles or unknown dependencies block readiness.
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
- Cycles or unknown dependencies block readiness.
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. Try post-install path first, fall back to pre-install:
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
- Cycles or unknown dependencies block readiness.
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.0",
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' \