devrites 3.0.0 → 3.0.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/engine/commands.md +1 -1
  4. package/engine/hooks_events_test.go +15 -0
  5. package/engine/hooks_workspace.go +8 -31
  6. package/engine/internal/gate/gate.go +4 -9
  7. package/engine/internal/gate/gate_test.go +16 -0
  8. package/engine/internal/lib/analyze.go +9 -10
  9. package/engine/internal/lib/buildreadiness.go +10 -16
  10. package/engine/internal/lib/checkacceptance.go +9 -9
  11. package/engine/internal/lib/coverage.go +13 -15
  12. package/engine/internal/lib/cursor_compat_test.go +91 -0
  13. package/engine/internal/lib/lanes.go +1 -1
  14. package/engine/internal/lib/progress.go +25 -24
  15. package/engine/internal/lib/resolve.go +9 -14
  16. package/engine/internal/lib/tickafk.go +11 -28
  17. package/engine/internal/migrate/migrate.go +28 -16
  18. package/engine/internal/migrate/migrate_test.go +5 -3
  19. package/engine/internal/state/cursor.go +84 -0
  20. package/engine/internal/state/cursor_test.go +58 -0
  21. package/engine/internal/state/feature.go +34 -31
  22. package/engine/internal/state/schema.go +30 -18
  23. package/engine/internal/state/snapshot.go +13 -2
  24. package/engine/internal/state/state_test.go +48 -3
  25. package/engine/internal/workflow/commands.go +25 -8
  26. package/engine/internal/workflow/commands_test.go +35 -0
  27. package/engine/testdata/golden/TestParityAnalyze/slug=noac.golden +1 -1
  28. package/engine/testdata/golden/TestParityCheckAcceptance/arg=flat-ok.golden +1 -1
  29. package/engine/tests/adr_0004_required_by_phase_test.go +7 -1
  30. package/engine/tests/canonical_ac_ids_test.go +44 -0
  31. package/engine/tests/gate_test.go +22 -0
  32. package/engine/tests/migrate_cli_test.go +3 -3
  33. package/pack/.claude/skills/rite-define/SKILL.md +6 -0
  34. package/pack/.claude/skills/rite-spec/SKILL.md +2 -0
  35. package/pack/.claude/skills/rite-vet/SKILL.md +6 -1
  36. package/pack/generated/claude/skills/rite-define/SKILL.md +6 -0
  37. package/pack/generated/claude/skills/rite-spec/SKILL.md +2 -0
  38. package/pack/generated/claude/skills/rite-vet/SKILL.md +6 -1
  39. package/pack/generated/codex/skills/rite-define/SKILL.md +6 -0
  40. package/pack/generated/codex/skills/rite-spec/SKILL.md +2 -0
  41. package/pack/generated/codex/skills/rite-vet/SKILL.md +6 -1
  42. package/package.json +1 -1
@@ -13,14 +13,19 @@ type Command struct {
13
13
  }
14
14
 
15
15
  var phaseToVerb = map[string]string{
16
- "frame": "frame",
17
- "spec": "spec",
18
- "plan": "define",
19
- "build": "build",
20
- "prove": "prove",
21
- "vet": "vet",
22
- "seal": "seal",
23
- "ship": "ship",
16
+ "frame": "frame",
17
+ "spec": "spec",
18
+ "temper": "temper",
19
+ "define": "define",
20
+ "plan": "define", // plan is the artifact state produced by rite-define.
21
+ "vet": "vet",
22
+ "build": "build",
23
+ "converge": "converge",
24
+ "prove": "prove",
25
+ "polish": "polish",
26
+ "review": "review",
27
+ "seal": "seal",
28
+ "ship": "ship",
24
29
  }
25
30
 
26
31
  // ForVerb returns the Claude and Codex forms for a public rite verb. The empty
@@ -35,6 +40,18 @@ func ForVerb(verb string) Command {
35
40
  return Command{Verb: verb, Claude: "/" + name, Codex: "$" + name}
36
41
  }
37
42
 
43
+ // ForAction extracts the first explicit rite invocation from a cursor action.
44
+ // Cursor values may append a short reason after the command.
45
+ func ForAction(action string) Command {
46
+ for _, field := range strings.Fields(action) {
47
+ field = strings.Trim(field, "`'\"()[]{}<>,.;:")
48
+ if strings.HasPrefix(field, "/rite-") || strings.HasPrefix(field, "$rite-") {
49
+ return ForVerb(field)
50
+ }
51
+ }
52
+ return Command{}
53
+ }
54
+
38
55
  // ForPhase returns the next public command for a workflow phase.
39
56
  func ForPhase(phase string) Command {
40
57
  verb := phaseToVerb[strings.TrimSpace(strings.ToLower(phase))]
@@ -0,0 +1,35 @@
1
+ package workflow
2
+
3
+ import "testing"
4
+
5
+ func TestForActionExtractsCanonicalInvocation(t *testing.T) {
6
+ for _, tc := range []struct {
7
+ action string
8
+ verb string
9
+ }{
10
+ {action: "/rite-define after readiness passes", verb: "define"},
11
+ {action: "resume with $rite-build", verb: "build"},
12
+ {action: "none"},
13
+ } {
14
+ if got := ForAction(tc.action); got.Verb != tc.verb {
15
+ t.Fatalf("ForAction(%q).Verb=%q, want %q", tc.action, got.Verb, tc.verb)
16
+ }
17
+ }
18
+ }
19
+
20
+ func TestForPhaseCoversCurrentLifecycle(t *testing.T) {
21
+ for _, phase := range []string{
22
+ "frame", "spec", "temper", "define", "vet", "build", "converge",
23
+ "prove", "polish", "review", "seal", "ship",
24
+ } {
25
+ if got := ForPhase(phase).Verb; got != phase {
26
+ t.Fatalf("ForPhase(%q).Verb=%q, want %q", phase, got, phase)
27
+ }
28
+ }
29
+ if got := ForPhase("plan").Verb; got != "define" {
30
+ t.Fatalf("ForPhase(plan).Verb=%q, want define compatibility route", got)
31
+ }
32
+ if got := ForPhase("done").Verb; got != "" {
33
+ t.Fatalf("ForPhase(done).Verb=%q, want empty", got)
34
+ }
35
+ }
@@ -2,7 +2,7 @@ exit 0
2
2
  # Cross-artifact analysis: noac
3
3
 
4
4
  ## Coverage
5
- - [warn] spec.md — no [ACn] acceptance ids found; tag criteria as '[AC1] ...' for a machine-checkable gate.
5
+ - [warn] spec.md — no AC-### acceptance ids found; tag criteria for a machine-checkable gate.
6
6
 
7
7
  ## Consistency
8
8
 
@@ -1,2 +1,2 @@
1
1
  exit 0
2
- check-acceptance: OK — 2 acceptance items all checked in seal.md (no ids; add [AC1].. for id-level grading)
2
+ check-acceptance: OK — 2 acceptance items all checked in seal.md (no ids; add AC-### for id-level grading)
@@ -16,12 +16,18 @@ import (
16
16
  var phaseArc = []state.Phase{
17
17
  state.PhaseFrame,
18
18
  state.PhaseSpec,
19
+ state.PhaseTemper,
20
+ state.PhaseDefine,
19
21
  state.PhasePlan,
22
+ state.PhaseVet,
20
23
  state.PhaseBuild,
24
+ state.PhaseConverge,
21
25
  state.PhaseProve,
22
- state.PhaseVet,
26
+ state.PhasePolish,
27
+ state.PhaseReview,
23
28
  state.PhaseSeal,
24
29
  state.PhaseShip,
30
+ state.PhaseDone,
25
31
  }
26
32
 
27
33
  func TestADR0004RequiredSectionsAreAdditiveDownTheArc(t *testing.T) {
@@ -0,0 +1,44 @@
1
+ package main_test
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestCanonicalAcceptanceIDsAcrossGates(t *testing.T) {
9
+ work := t.TempDir()
10
+ writeFeatureFile(t, work, "canonical", "spec.md",
11
+ "## Acceptance criteria\n- [ ] AC-001: alpha\n- [ ] AC-002: beta\n")
12
+ writeFeatureFile(t, work, "canonical", "tasks.md",
13
+ "## SLICE-001 alpha\nSatisfies: AC-001\n## SLICE-002 beta\nSatisfies: AC-002\n## SLICE-003 orphan\n")
14
+ writeFeatureFile(t, work, "canonical", "seal.md",
15
+ "## Acceptance Criteria\n- [x] AC-001: alpha — evidence: t1\n- [x] AC-002: beta — evidence: t2\n")
16
+
17
+ t.Run("analyze", func(t *testing.T) {
18
+ stdout, stderr, code := runGo(t, work, "analyze", "canonical")
19
+ if code != 0 || !strings.Contains(stdout, "AC-001 — covered") || !strings.Contains(stdout, "slice 'SLICE-003 orphan'") {
20
+ t.Fatalf("exit=%d stdout=%q stderr=%q", code, stdout, stderr)
21
+ }
22
+ })
23
+
24
+ t.Run("coverage", func(t *testing.T) {
25
+ stdout, stderr, code := runGo(t, work, "coverage", "canonical")
26
+ if code != 0 || !strings.Contains(stdout, "| AC-001 | SLICE-001 alpha | yes |") {
27
+ t.Fatalf("exit=%d stdout=%q stderr=%q", code, stdout, stderr)
28
+ }
29
+ })
30
+
31
+ t.Run("check-acceptance", func(t *testing.T) {
32
+ stdout, stderr, code := runGo(t, work, "check-acceptance", ".devrites/features/canonical")
33
+ if code != 0 || !strings.Contains(stdout, "AC-001 AC-002") {
34
+ t.Fatalf("exit=%d stdout=%q stderr=%q", code, stdout, stderr)
35
+ }
36
+ })
37
+
38
+ t.Run("lanes", func(t *testing.T) {
39
+ stdout, stderr, code := runGo(t, work, "lanes", "plan", "canonical")
40
+ if code != 0 || !strings.Contains(stdout, "- SLICE-001 alpha:") {
41
+ t.Fatalf("exit=%d stdout=%q stderr=%q", code, stdout, stderr)
42
+ }
43
+ })
44
+ }
@@ -85,6 +85,28 @@ func TestReadinessPassesCompletePhase(t *testing.T) {
85
85
  }
86
86
  }
87
87
 
88
+ func TestReadinessAcceptsCanonicalTemperCursor(t *testing.T) {
89
+ root := filepath.Join(t.TempDir(), ".devrites")
90
+ testutil.WriteFile(t, filepath.Join(root, "work", "tempered", "state.md"), `# State
91
+
92
+ ## Cursor
93
+ | Key | Value |
94
+ | --- | --- |
95
+ | phase | temper |
96
+ | status | running |
97
+ | next_action | /rite-define |
98
+ `)
99
+ testutil.WriteFile(t, filepath.Join(root, "work", "tempered", "spec.md"), "# Spec\n\nReady.\n")
100
+
101
+ out, errOut, code := runDevrites(t, root, "readiness", "tempered")
102
+ if code != 0 {
103
+ t.Fatalf("exit = %d, want 0; stderr:\n%s", code, errOut)
104
+ }
105
+ if !strings.Contains(out, "phase: temper") || !strings.Contains(out, "result: pass") {
106
+ t.Fatalf("unexpected readiness output:\n%s", out)
107
+ }
108
+ }
109
+
88
110
  func TestSealBlocksWhenSealSectionsMissing(t *testing.T) {
89
111
  root := newWorkspace(t)
90
112
  out, _, code := runDevrites(t, root, "seal", "auth-tokens")
@@ -20,7 +20,7 @@ func workWorkspace(t *testing.T) (root, slug string) {
20
20
  t.Fatal(err)
21
21
  }
22
22
  files := map[string]string{
23
- "state.md": "# State\n\nphase: build\n\nWorking on it.\n",
23
+ "state.md": "# State\n\n| Key | Value |\n| --- | --- |\n| phase | temper |\n| status | running |\n",
24
24
  "spec.md": "# Spec\n\nRotate tokens.\n",
25
25
  "plan.md": "# Plan\n\nStep 1, step 2.\n",
26
26
  "tasks.md": "# Tasks\n\n- [x] one\n",
@@ -68,7 +68,7 @@ func TestMigrateNormalizesCanonicalWorkSchema(t *testing.T) {
68
68
  if err != nil {
69
69
  t.Fatal(err)
70
70
  }
71
- if !strings.Contains(string(fm), "phase: build") {
71
+ if !strings.Contains(string(fm), "phase: temper") {
72
72
  t.Errorf("feature.md phase not derived from state.md\n%s", fm)
73
73
  }
74
74
  if !strings.Contains(string(fm), "schemaVersion: 1") {
@@ -94,7 +94,7 @@ func TestMigratePostStatusWorks(t *testing.T) {
94
94
  if code != 0 {
95
95
  t.Fatalf("status after migrate exit = %d (stderr: %s)", code, errOut)
96
96
  }
97
- if !strings.Contains(out, "phase: build") {
97
+ if !strings.Contains(out, "phase: temper") {
98
98
  t.Errorf("migrated feature status is wrong\n%s", out)
99
99
  }
100
100
  }
@@ -119,6 +119,12 @@ Pull these via `Read` when shaping the plan:
119
119
  as a blocking gate at `/rite-vet`; no file → none declared → nothing to check.)
120
120
  6. **Write** `architecture.md`, `plan.md`, `tasks.md`, and `traceability.md`; update
121
121
  `state.md` (phase: plan → next `/rite-vet`).
122
+ 6a. **Cross-artifact gate — now that `tasks.md` exists.** Run the deterministic
123
+ spec↔tasks coverage/consistency check; any non-zero result blocks plan readiness:
124
+ ```bash
125
+ S="$(cat .devrites/ACTIVE 2>/dev/null)"
126
+ devrites-engine analyze "$S"
127
+ ```
122
128
  7. **Readiness gate** (bottom of plan-template): every acceptance criterion covered by a
123
129
  slice, dependency order acyclic + risk-first, no unjustified deviation, rollback for
124
130
  every destructive/migration step. **Stop and confirm** before code. Render the review-before-code
@@ -185,6 +185,8 @@ stay flat `AC-###` bullets; the grammar is opt-in by rigor, never forced.
185
185
  devrites-engine spec-skeleton ".devrites/work/<slug>"
186
186
  devrites-engine spec-validate ".devrites/work/<slug>" --against .devrites/specs
187
187
  ```
188
+ **Do not run `devrites-engine analyze` in this phase:** `tasks.md` deliberately does not
189
+ exist yet. `/rite-define` owns the first analyze pass after it writes the slices.
188
190
  Treat edge/prohibition findings as blocking just like grammar findings. When it passes, write `Spec gate: passed <iso>` to `state.md`.
189
191
  6a. **Review-before-code digest.** Before handing off to planning, render the cheap human review:
190
192
  `Intent` (one sentence), `Done means` (top acceptance/scenario IDs), `Scope/risk` (what is in/out
@@ -140,7 +140,12 @@ Audit for a plan resting on unstated beliefs).
140
140
  (`/rite-build` and `/rite-prove` read it). Harden `plan.md` / `tasks.md` directly for
141
141
  behavior-preserving refinements; route every acceptance/behavior-changing delta through the
142
142
  **Spec Drift Guard** (`drift.md` + recorded decision + `/rite-plan repair`). Append
143
- `decisions.md` (one ADR per material call) and `assumptions.md`. Update `state.md`:
143
+ `decisions.md` (one ADR per material call) and `assumptions.md`. Re-run the gate after
144
+ every fold-back so a task edit cannot invalidate the earlier pass:
145
+ ```bash
146
+ devrites-engine analyze; echo "final analyze rc=$?"
147
+ ```
148
+ Any non-zero result blocks the handoff. Then update `state.md`:
144
149
  `Phase: vet`, `Next step: /rite-build`; on a blocking pause write the `Awaiting human` block +
145
150
  `Status: awaiting_human` before stopping.
146
151
  6. **Adversarial verification loop.** Apply the light/full branching contract in
@@ -119,6 +119,12 @@ Pull these via `Read` when shaping the plan:
119
119
  as a blocking gate at `/rite-vet`; no file → none declared → nothing to check.)
120
120
  6. **Write** `architecture.md`, `plan.md`, `tasks.md`, and `traceability.md`; update
121
121
  `state.md` (phase: plan → next `/rite-vet`).
122
+ 6a. **Cross-artifact gate — now that `tasks.md` exists.** Run the deterministic
123
+ spec↔tasks coverage/consistency check; any non-zero result blocks plan readiness:
124
+ ```bash
125
+ S="$(cat .devrites/ACTIVE 2>/dev/null)"
126
+ devrites-engine analyze "$S"
127
+ ```
122
128
  7. **Readiness gate** (bottom of plan-template): every acceptance criterion covered by a
123
129
  slice, dependency order acyclic + risk-first, no unjustified deviation, rollback for
124
130
  every destructive/migration step. **Stop and confirm** before code. Render the review-before-code
@@ -185,6 +185,8 @@ stay flat `AC-###` bullets; the grammar is opt-in by rigor, never forced.
185
185
  devrites-engine spec-skeleton ".devrites/work/<slug>"
186
186
  devrites-engine spec-validate ".devrites/work/<slug>" --against .devrites/specs
187
187
  ```
188
+ **Do not run `devrites-engine analyze` in this phase:** `tasks.md` deliberately does not
189
+ exist yet. `/rite-define` owns the first analyze pass after it writes the slices.
188
190
  Treat edge/prohibition findings as blocking just like grammar findings. When it passes, write `Spec gate: passed <iso>` to `state.md`.
189
191
  6a. **Review-before-code digest.** Before handing off to planning, render the cheap human review:
190
192
  `Intent` (one sentence), `Done means` (top acceptance/scenario IDs), `Scope/risk` (what is in/out
@@ -140,7 +140,12 @@ Audit for a plan resting on unstated beliefs).
140
140
  (`/rite-build` and `/rite-prove` read it). Harden `plan.md` / `tasks.md` directly for
141
141
  behavior-preserving refinements; route every acceptance/behavior-changing delta through the
142
142
  **Spec Drift Guard** (`drift.md` + recorded decision + `/rite-plan repair`). Append
143
- `decisions.md` (one ADR per material call) and `assumptions.md`. Update `state.md`:
143
+ `decisions.md` (one ADR per material call) and `assumptions.md`. Re-run the gate after
144
+ every fold-back so a task edit cannot invalidate the earlier pass:
145
+ ```bash
146
+ devrites-engine analyze; echo "final analyze rc=$?"
147
+ ```
148
+ Any non-zero result blocks the handoff. Then update `state.md`:
144
149
  `Phase: vet`, `Next step: /rite-build`; on a blocking pause write the `Awaiting human` block +
145
150
  `Status: awaiting_human` before stopping.
146
151
  6. **Adversarial verification loop.** Apply the light/full branching contract in
@@ -131,6 +131,12 @@ Pull these via `Read` when shaping the plan:
131
131
  as a blocking gate at `$rite-vet`; no file → none declared → nothing to check.)
132
132
  6. **Write** `architecture.md`, `plan.md`, `tasks.md`, and `traceability.md`; update
133
133
  `state.md` (phase: plan → next `$rite-vet`).
134
+ 6a. **Cross-artifact gate — now that `tasks.md` exists.** Run the deterministic
135
+ spec↔tasks coverage/consistency check; any non-zero result blocks plan readiness:
136
+ ```bash
137
+ S="$(cat .devrites/ACTIVE 2>/dev/null)"
138
+ devrites-engine analyze "$S"
139
+ ```
134
140
  7. **Readiness gate** (bottom of plan-template): every acceptance criterion covered by a
135
141
  slice, dependency order acyclic + risk-first, no unjustified deviation, rollback for
136
142
  every destructive/migration step. **Stop and confirm** before code. Render the review-before-code
@@ -197,6 +197,8 @@ stay flat `AC-###` bullets; the grammar is opt-in by rigor, never forced.
197
197
  devrites-engine spec-skeleton ".devrites/work/<slug>"
198
198
  devrites-engine spec-validate ".devrites/work/<slug>" --against .devrites/specs
199
199
  ```
200
+ **Do not run `devrites-engine analyze` in this phase:** `tasks.md` deliberately does not
201
+ exist yet. `$rite-define` owns the first analyze pass after it writes the slices.
200
202
  Treat edge/prohibition findings as blocking just like grammar findings. When it passes, write `Spec gate: passed <iso>` to `state.md`.
201
203
  6a. **Review-before-code digest.** Before handing off to planning, render the cheap human review:
202
204
  `Intent` (one sentence), `Done means` (top acceptance/scenario IDs), `Scope/risk` (what is in/out
@@ -152,7 +152,12 @@ Audit for a plan resting on unstated beliefs).
152
152
  (`$rite-build` and `$rite-prove` read it). Harden `plan.md` / `tasks.md` directly for
153
153
  behavior-preserving refinements; route every acceptance/behavior-changing delta through the
154
154
  **Spec Drift Guard** (`drift.md` + recorded decision + `$rite-plan repair`). Append
155
- `decisions.md` (one ADR per material call) and `assumptions.md`. Update `state.md`:
155
+ `decisions.md` (one ADR per material call) and `assumptions.md`. Re-run the gate after
156
+ every fold-back so a task edit cannot invalidate the earlier pass:
157
+ ```bash
158
+ devrites-engine analyze; echo "final analyze rc=$?"
159
+ ```
160
+ Any non-zero result blocks the handoff. Then update `state.md`:
156
161
  `Phase: vet`, `Next step: $rite-build`; on a blocking pause write the `Awaiting human` block +
157
162
  `Status: awaiting_human` before stopping.
158
163
  6. **Adversarial verification loop.** Apply the light/full branching contract in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "DevRites — disciplined senior-engineer workflow skills pack for Claude Code and Codex",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://github.com/ViktorsBaikers/DevRites#readme",