devrites 3.0.1 → 3.0.3

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 (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/adr/0004-state-schema-phases-sections.md +44 -0
  4. package/docs/command-map.md +1 -1
  5. package/docs/engine/state-schema.md +15 -12
  6. package/docs/flow.md +1 -1
  7. package/docs/research/go-authoritative-workflow-schema-and-quality-2026-07-20.md +212 -0
  8. package/engine/hooks_events_test.go +15 -0
  9. package/engine/hooks_workspace.go +8 -31
  10. package/engine/internal/gate/gate.go +6 -11
  11. package/engine/internal/gate/gate_test.go +18 -2
  12. package/engine/internal/lib/buildreadiness.go +10 -16
  13. package/engine/internal/lib/cursor_compat_test.go +120 -0
  14. package/engine/internal/lib/preamble.go +1 -1
  15. package/engine/internal/lib/preamble_questions_test.go +10 -0
  16. package/engine/internal/lib/progress.go +69 -29
  17. package/engine/internal/lib/resolve.go +21 -17
  18. package/engine/internal/lib/resolve_remediation_test.go +9 -0
  19. package/engine/internal/lib/tickafk.go +11 -28
  20. package/engine/internal/migrate/migrate.go +13 -32
  21. package/engine/internal/migrate/migrate_test.go +11 -3
  22. package/engine/internal/state/cmd/workflowmanifest/main.go +54 -0
  23. package/engine/internal/state/cursor.go +119 -0
  24. package/engine/internal/state/cursor_test.go +58 -0
  25. package/engine/internal/state/feature.go +25 -48
  26. package/engine/internal/state/schema.go +151 -24
  27. package/engine/internal/state/snapshot.go +60 -3
  28. package/engine/internal/state/state_test.go +119 -3
  29. package/engine/internal/state/workflow_manifest.json +310 -0
  30. package/engine/internal/workflow/commands.go +10 -29
  31. package/engine/internal/workflow/commands_test.go +18 -0
  32. package/engine/testdata/golden/TestParityProgress/arg=allbuilt.golden +1 -1
  33. package/engine/testdata/golden/TestParityProgress/arg=done.golden +1 -1
  34. package/engine/testdata/golden/TestParityProgress/arg=mid.golden +1 -1
  35. package/engine/testdata/golden/TestParityProgress/arg=nophase.golden +1 -1
  36. package/engine/testdata/golden/TestParityProgress/arg=noslice.golden +1 -1
  37. package/engine/testdata/golden/TestParityProgress/arg=plan.golden +1 -1
  38. package/engine/testdata/golden/TestParityProgress/arg=seal.golden +1 -1
  39. package/engine/tests/adr_0004_required_by_phase_test.go +1 -12
  40. package/engine/tests/gate_test.go +22 -0
  41. package/engine/tests/lib_parity_test.go +1 -1
  42. package/engine/tests/migrate_cli_test.go +3 -3
  43. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
  44. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
  45. package/pack/generated/codex/AGENTS.md +1 -1
  46. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
  47. package/package.json +1 -1
  48. package/scripts/codex-generate.sh +1 -1
  49. package/scripts/grade-feature.sh +5 -3
  50. package/scripts/run-outcome-evals.sh +21 -0
  51. package/scripts/validate-workspace-schema.py +9 -73
  52. package/scripts/validate.sh +10 -0
  53. package/scripts/workflow_schema.py +69 -0
@@ -29,6 +29,56 @@ func TestRequiredSectionsIsPhaseRelativeAndOrdered(t *testing.T) {
29
29
  }
30
30
  }
31
31
 
32
+ func TestLifecycleRegistryOwnsOrderAndResumeCommands(t *testing.T) {
33
+ phases := LifecyclePhases()
34
+ if len(phases) == 0 || phases[0] != PhaseFrame || phases[len(phases)-1] != PhaseDone {
35
+ t.Fatalf("LifecyclePhases()=%v, want frame...done", phases)
36
+ }
37
+ if got := ResumeVerb(PhasePlan); got != "define" {
38
+ t.Fatalf("ResumeVerb(plan)=%q, want define", got)
39
+ }
40
+ if got := ResumeVerb(PhaseDone); got != "" {
41
+ t.Fatalf("ResumeVerb(done)=%q, want empty", got)
42
+ }
43
+
44
+ phases[0] = PhaseDone
45
+ if got := LifecyclePhases()[0]; got != PhaseFrame {
46
+ t.Fatalf("LifecyclePhases exposed mutable registry: first=%q", got)
47
+ }
48
+ }
49
+
50
+ func TestLifecycleRegistryInvariants(t *testing.T) {
51
+ phaseNames := map[Phase]bool{}
52
+ aliases := map[string]Phase{}
53
+ for i, definition := range phaseDefinitions {
54
+ if definition.phase == "" || phaseNames[definition.phase] {
55
+ t.Fatalf("phase definition %d has empty or duplicate ID %q", i, definition.phase)
56
+ }
57
+ phaseNames[definition.phase] = true
58
+ if len(definition.workspaceRequired) == 0 {
59
+ t.Fatalf("phase %q has no workspace requirements", definition.phase)
60
+ }
61
+ for _, alias := range definition.aliases {
62
+ if alias == "" || aliases[alias] != "" || KnownPhase(Phase(alias)) {
63
+ t.Fatalf("phase %q has empty or duplicate alias %q", definition.phase, alias)
64
+ }
65
+ aliases[alias] = definition.phase
66
+ }
67
+ for _, section := range definition.required {
68
+ known := false
69
+ for _, canonical := range Sections {
70
+ known = known || section == canonical
71
+ }
72
+ if !known {
73
+ t.Fatalf("phase %q requires unknown section %q", definition.phase, section)
74
+ }
75
+ }
76
+ if definition.shippable && !definition.proofRequired {
77
+ t.Fatalf("shippable phase %q does not require proof", definition.phase)
78
+ }
79
+ }
80
+ }
81
+
32
82
  func TestStatusFixtureBuildIncomplete(t *testing.T) {
33
83
  rep, err := Status(fixtureRoot, "auth-tokens")
34
84
  if err != nil {
@@ -126,6 +176,73 @@ func TestLoadFeatureFromLedgerAndAliases(t *testing.T) {
126
176
  }
127
177
  }
128
178
 
179
+ func TestLedgerPhaseOverridesStaleManifestPhase(t *testing.T) {
180
+ root := filepath.Join(t.TempDir(), ".devrites")
181
+ writeWorkSection(t, root, "live", "feature.md", "---\nphase: spec\nschemaVersion: 1\n---\n")
182
+ writeWorkSection(t, root, "live", "state.md", "| Key | Value |\n| --- | --- |\n| phase | temper |\n")
183
+ writeWorkSection(t, root, "live", "spec.md", "# Spec\n\nReady.\n")
184
+
185
+ rep, err := Status(root, "live")
186
+ if err != nil {
187
+ t.Fatal(err)
188
+ }
189
+ if rep.Phase != PhaseTemper {
190
+ t.Fatalf("phase=%q, want current ledger phase %q", rep.Phase, PhaseTemper)
191
+ }
192
+ }
193
+
194
+ func TestSnapshotUsesCanonicalNextActionAndWarnsWhenRequiredProofMissing(t *testing.T) {
195
+ root := filepath.Join(t.TempDir(), ".devrites")
196
+ writeWorkSection(t, root, "live", "state.md", `# State
197
+
198
+ | Key | Value |
199
+ | --- | --- |
200
+ | phase | review |
201
+ | status | running |
202
+ | next_action | /rite-seal after review is clean |
203
+ `)
204
+ for name, body := range map[string]string{
205
+ "spec.md": "# Spec\n\nReady.\n",
206
+ "plan.md": "# Plan\n\nReady.\n",
207
+ "decisions.md": "# Decisions\n\nReady.\n",
208
+ "tasks.md": "# Tasks\n\nReady.\n",
209
+ } {
210
+ writeWorkSection(t, root, "live", name, body)
211
+ }
212
+
213
+ snap, err := Snapshot(root, "live")
214
+ if err != nil {
215
+ t.Fatal(err)
216
+ }
217
+ if snap.NextCommands.Verb != "seal" || snap.NextCommand != "/rite-seal" {
218
+ t.Fatalf("next commands=%+v legacy=%q, want canonical next_action seal", snap.NextCommands, snap.NextCommand)
219
+ }
220
+ if got := strings.Join(snap.Warnings, "\n"); !strings.Contains(got, "requires fresh evidence") {
221
+ t.Fatalf("warnings=%v, want missing required-proof warning", snap.Warnings)
222
+ }
223
+ }
224
+
225
+ func TestSnapshotReadsCanonicalActiveSliceAndCountsQuestionsByRecord(t *testing.T) {
226
+ workDir := t.TempDir()
227
+ if err := os.WriteFile(filepath.Join(workDir, "state.md"), []byte("| phase | build |\n| active_slice | SLICE-002 |\n"), 0o644); err != nil {
228
+ t.Fatal(err)
229
+ }
230
+ if err := os.WriteFile(filepath.Join(workDir, "tasks.md"), []byte("## SLICE-001 First\n\n## SLICE-002 Second\n"), 0o644); err != nil {
231
+ t.Fatal(err)
232
+ }
233
+ if err := os.WriteFile(filepath.Join(workDir, "questions.md"), []byte("## Q-001\nstatus: open\ngate: blocking\n\n## Q-002\nstatus: answered\ngate: blocking\n"), 0o644); err != nil {
234
+ t.Fatal(err)
235
+ }
236
+
237
+ slice := currentSlice(workDir)
238
+ if slice == nil || slice.Name != "SLICE-002" || slice.Index != 2 || slice.Total != 2 {
239
+ t.Fatalf("currentSlice=%+v, want canonical SLICE-002 at 2/2", slice)
240
+ }
241
+ if drift := driftSummary(workDir); drift.Status != "open" || drift.Open != 1 {
242
+ t.Fatalf("driftSummary=%+v, want one open question record", drift)
243
+ }
244
+ }
245
+
129
246
  func TestWorkLayoutIsCanonicalAndFeaturesIsAlias(t *testing.T) {
130
247
  root := filepath.Join(t.TempDir(), ".devrites")
131
248
  writeWorkSection(t, root, "live", "state.md", "- Phase: build\n")
@@ -148,9 +265,8 @@ func TestWorkLayoutIsCanonicalAndFeaturesIsAlias(t *testing.T) {
148
265
  }
149
266
  }
150
267
 
151
- // An unknown phase word in the ledger is ignored (not accepted as a phase), so a
152
- // ledger-only feature with no recognizable phase is a clear error, not a silent
153
- // mis-load.
268
+ // An unknown phase word in the ledger is rejected, so a ledger-only feature is a
269
+ // clear error rather than silently falling back or mis-loading.
154
270
  func TestLedgerPhaseRejectsUnknownWord(t *testing.T) {
155
271
  root := filepath.Join(t.TempDir(), ".devrites")
156
272
  writeSection(t, root, "bogus", "state.md", "- Phase: banana\n")
@@ -0,0 +1,310 @@
1
+ {
2
+ "generatedBy": "go generate ./internal/state; DO NOT EDIT",
3
+ "schemaVersion": 1,
4
+ "phases": [
5
+ {
6
+ "id": "frame",
7
+ "resumeVerb": "frame",
8
+ "workspaceRequired": [
9
+ "state.md"
10
+ ]
11
+ },
12
+ {
13
+ "id": "spec",
14
+ "resumeVerb": "spec",
15
+ "aliases": [
16
+ "specced",
17
+ "specifying"
18
+ ],
19
+ "workspaceRequired": [
20
+ "brief.md",
21
+ "spec.md",
22
+ "state.md",
23
+ "decisions.md",
24
+ "assumptions.md",
25
+ "questions.md"
26
+ ]
27
+ },
28
+ {
29
+ "id": "temper",
30
+ "resumeVerb": "temper",
31
+ "aliases": [
32
+ "tempered",
33
+ "tempering"
34
+ ],
35
+ "workspaceRequired": [
36
+ "brief.md",
37
+ "spec.md",
38
+ "state.md",
39
+ "decisions.md",
40
+ "assumptions.md",
41
+ "questions.md"
42
+ ]
43
+ },
44
+ {
45
+ "id": "define",
46
+ "resumeVerb": "define",
47
+ "aliases": [
48
+ "defined",
49
+ "defining"
50
+ ],
51
+ "workspaceRequired": [
52
+ "brief.md",
53
+ "spec.md",
54
+ "architecture.md",
55
+ "plan.md",
56
+ "tasks.md",
57
+ "traceability.md",
58
+ "state.md",
59
+ "decisions.md",
60
+ "assumptions.md",
61
+ "questions.md"
62
+ ],
63
+ "blocksOpenQuestions": true
64
+ },
65
+ {
66
+ "id": "plan",
67
+ "resumeVerb": "define",
68
+ "aliases": [
69
+ "planned",
70
+ "planning"
71
+ ],
72
+ "workspaceRequired": [
73
+ "brief.md",
74
+ "spec.md",
75
+ "architecture.md",
76
+ "plan.md",
77
+ "tasks.md",
78
+ "traceability.md",
79
+ "state.md",
80
+ "decisions.md",
81
+ "assumptions.md",
82
+ "questions.md"
83
+ ],
84
+ "blocksOpenQuestions": true
85
+ },
86
+ {
87
+ "id": "vet",
88
+ "resumeVerb": "vet",
89
+ "aliases": [
90
+ "vetted",
91
+ "vetting"
92
+ ],
93
+ "workspaceRequired": [
94
+ "brief.md",
95
+ "spec.md",
96
+ "architecture.md",
97
+ "plan.md",
98
+ "tasks.md",
99
+ "traceability.md",
100
+ "state.md",
101
+ "decisions.md",
102
+ "assumptions.md",
103
+ "questions.md"
104
+ ],
105
+ "blocksOpenQuestions": true
106
+ },
107
+ {
108
+ "id": "build",
109
+ "resumeVerb": "build",
110
+ "aliases": [
111
+ "building",
112
+ "wip",
113
+ "in",
114
+ "in-progress"
115
+ ],
116
+ "workspaceRequired": [
117
+ "brief.md",
118
+ "spec.md",
119
+ "architecture.md",
120
+ "plan.md",
121
+ "tasks.md",
122
+ "traceability.md",
123
+ "state.md",
124
+ "decisions.md",
125
+ "assumptions.md",
126
+ "questions.md"
127
+ ],
128
+ "blocksOpenQuestions": true
129
+ },
130
+ {
131
+ "id": "converge",
132
+ "resumeVerb": "converge",
133
+ "aliases": [
134
+ "converged",
135
+ "converging"
136
+ ],
137
+ "workspaceRequired": [
138
+ "brief.md",
139
+ "spec.md",
140
+ "architecture.md",
141
+ "plan.md",
142
+ "tasks.md",
143
+ "traceability.md",
144
+ "state.md",
145
+ "decisions.md",
146
+ "assumptions.md",
147
+ "questions.md"
148
+ ],
149
+ "blocksOpenQuestions": true
150
+ },
151
+ {
152
+ "id": "prove",
153
+ "resumeVerb": "prove",
154
+ "aliases": [
155
+ "proving",
156
+ "proven",
157
+ "testing"
158
+ ],
159
+ "workspaceRequired": [
160
+ "brief.md",
161
+ "spec.md",
162
+ "architecture.md",
163
+ "plan.md",
164
+ "tasks.md",
165
+ "traceability.md",
166
+ "state.md",
167
+ "decisions.md",
168
+ "assumptions.md",
169
+ "questions.md",
170
+ "evidence.md",
171
+ "touched-files.md"
172
+ ],
173
+ "proofRequired": true,
174
+ "blocksOpenQuestions": true
175
+ },
176
+ {
177
+ "id": "polish",
178
+ "resumeVerb": "polish",
179
+ "aliases": [
180
+ "polished",
181
+ "polishing"
182
+ ],
183
+ "workspaceRequired": [
184
+ "brief.md",
185
+ "spec.md",
186
+ "architecture.md",
187
+ "plan.md",
188
+ "tasks.md",
189
+ "traceability.md",
190
+ "state.md",
191
+ "decisions.md",
192
+ "assumptions.md",
193
+ "questions.md",
194
+ "evidence.md",
195
+ "touched-files.md"
196
+ ],
197
+ "proofRequired": true,
198
+ "blocksOpenQuestions": true
199
+ },
200
+ {
201
+ "id": "review",
202
+ "resumeVerb": "review",
203
+ "aliases": [
204
+ "reviewed",
205
+ "reviewing"
206
+ ],
207
+ "workspaceRequired": [
208
+ "brief.md",
209
+ "spec.md",
210
+ "architecture.md",
211
+ "plan.md",
212
+ "tasks.md",
213
+ "traceability.md",
214
+ "state.md",
215
+ "decisions.md",
216
+ "assumptions.md",
217
+ "questions.md",
218
+ "evidence.md",
219
+ "touched-files.md"
220
+ ],
221
+ "proofRequired": true,
222
+ "blocksOpenQuestions": true
223
+ },
224
+ {
225
+ "id": "seal",
226
+ "resumeVerb": "seal",
227
+ "aliases": [
228
+ "sealed",
229
+ "sealing"
230
+ ],
231
+ "workspaceRequired": [
232
+ "brief.md",
233
+ "spec.md",
234
+ "architecture.md",
235
+ "plan.md",
236
+ "tasks.md",
237
+ "traceability.md",
238
+ "state.md",
239
+ "decisions.md",
240
+ "assumptions.md",
241
+ "questions.md",
242
+ "evidence.md",
243
+ "touched-files.md"
244
+ ],
245
+ "proofRequired": true,
246
+ "blocksOpenQuestions": true,
247
+ "shippable": true
248
+ },
249
+ {
250
+ "id": "ship",
251
+ "resumeVerb": "ship",
252
+ "aliases": [
253
+ "shipped",
254
+ "shipping"
255
+ ],
256
+ "workspaceRequired": [
257
+ "brief.md",
258
+ "spec.md",
259
+ "architecture.md",
260
+ "plan.md",
261
+ "tasks.md",
262
+ "traceability.md",
263
+ "state.md",
264
+ "decisions.md",
265
+ "assumptions.md",
266
+ "questions.md",
267
+ "evidence.md",
268
+ "touched-files.md"
269
+ ],
270
+ "proofRequired": true,
271
+ "blocksOpenQuestions": true,
272
+ "shippable": true
273
+ },
274
+ {
275
+ "id": "done",
276
+ "aliases": [
277
+ "closed",
278
+ "complete",
279
+ "completed"
280
+ ],
281
+ "workspaceRequired": [
282
+ "brief.md",
283
+ "spec.md",
284
+ "architecture.md",
285
+ "plan.md",
286
+ "tasks.md",
287
+ "traceability.md",
288
+ "state.md",
289
+ "decisions.md",
290
+ "assumptions.md",
291
+ "questions.md",
292
+ "evidence.md",
293
+ "touched-files.md"
294
+ ],
295
+ "proofRequired": true,
296
+ "blocksOpenQuestions": true,
297
+ "shippable": true
298
+ }
299
+ ],
300
+ "cursorKeyAliases": [
301
+ {
302
+ "alias": "nextstep",
303
+ "canonical": "next_action"
304
+ },
305
+ {
306
+ "alias": "qid",
307
+ "canonical": "question_id"
308
+ }
309
+ ]
310
+ }
@@ -12,17 +12,6 @@ type Command struct {
12
12
  Codex string
13
13
  }
14
14
 
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",
24
- }
25
-
26
15
  // ForVerb returns the Claude and Codex forms for a public rite verb. The empty
27
16
  // verb returns an empty command so callers can omit suggestions safely.
28
17
  func ForVerb(verb string) Command {
@@ -35,10 +24,16 @@ func ForVerb(verb string) Command {
35
24
  return Command{Verb: verb, Claude: "/" + name, Codex: "$" + name}
36
25
  }
37
26
 
38
- // ForPhase returns the next public command for a workflow phase.
39
- func ForPhase(phase string) Command {
40
- verb := phaseToVerb[strings.TrimSpace(strings.ToLower(phase))]
41
- return ForVerb(verb)
27
+ // ForAction extracts the first explicit rite invocation from a cursor action.
28
+ // Cursor values may append a short reason after the command.
29
+ func ForAction(action string) Command {
30
+ for _, field := range strings.Fields(action) {
31
+ field = strings.Trim(field, "`'\"()[]{}<>,.;:")
32
+ if strings.HasPrefix(field, "/rite-") || strings.HasPrefix(field, "$rite-") {
33
+ return ForVerb(field)
34
+ }
35
+ }
36
+ return Command{}
42
37
  }
43
38
 
44
39
  // Both renders a command in host-neutral prose for engine stderr/stdout messages.
@@ -48,17 +43,3 @@ func (c Command) Both() string {
48
43
  }
49
44
  return c.Claude + " (Claude) / " + c.Codex + " (Codex)"
50
45
  }
51
-
52
- // ClaudeOrCodex returns the command for a known harness. Empty/unknown harnesses
53
- // deliberately get the host-neutral rendering so CLI output remains safe when the
54
- // engine is run outside a hook.
55
- func (c Command) ClaudeOrCodex(harness string) string {
56
- switch strings.ToLower(strings.TrimSpace(harness)) {
57
- case "claude", "claude-code", "claudecode":
58
- return c.Claude
59
- case "codex":
60
- return c.Codex
61
- default:
62
- return c.Both()
63
- }
64
- }
@@ -0,0 +1,18 @@
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
+ }
@@ -1,4 +1,4 @@
1
1
  exit 0
2
2
  ── rite-build ─────────
3
3
  Slices 2/2 ██████████ ✅ ALL BUILT
4
- Flow spec ✓ define ✓ build ◉ prove ○ polish ○ review ○ seal ○ ship ○
4
+ Flow spec ✓ define ✓ plan ✓ build ◉ prove ○ polish ○ review ○ seal ○ ship ○
@@ -1,3 +1,3 @@
1
1
  exit 0
2
2
  ── rite-done ─────────
3
- Flow spec ✓ define ✓ build ✓ prove ✓ polish ✓ review ✓ seal ✓ ship ✓
3
+ Flow spec ✓ define ✓ plan ✓ build ✓ prove ✓ polish ✓ review ✓ seal ✓ ship ✓
@@ -1,4 +1,4 @@
1
1
  exit 0
2
2
  ── rite-build ─────────
3
3
  Slice 2/3 ███████░░░ detail ✓
4
- Flow spec ✓ temper ✓ define ✓ vet ✓ build ◉ prove ○ polish ○ review ○ seal ○ ship ○
4
+ Flow spec ✓ temper ✓ define ✓ plan ✓ vet ✓ build ◉ prove ○ polish ○ review ○ seal ○ ship ○
@@ -1,3 +1,3 @@
1
1
  exit 0
2
2
  ── rite-spec ─────────
3
- Flow spec ◉ define ○ build ○ prove ○ polish ○ review ○ seal ○ ship ○
3
+ Flow spec ◉ define ○ plan ○ build ○ prove ○ polish ○ review ○ seal ○ ship ○
@@ -1,3 +1,3 @@
1
1
  exit 0
2
2
  ── rite-spec ─────────
3
- Flow spec ◉ define ○ build ○ prove ○ polish ○ review ○ seal ○ ship ○
3
+ Flow spec ◉ define ○ plan ○ build ○ prove ○ polish ○ review ○ seal ○ ship ○
@@ -1,4 +1,4 @@
1
1
  exit 0
2
2
  ── rite-plan ─────────
3
3
  Slice 2/3 ███████░░░ detail ✓
4
- Flow spec ✓ define ✓ build ◉ prove ○ polish ○ review ○ seal ○ ship ○
4
+ Flow spec ✓ define ✓ planbuild ○ prove ○ polish ○ review ○ seal ○ ship ○
@@ -1,3 +1,3 @@
1
1
  exit 0
2
2
  ── rite-seal ─────────
3
- Flow spec ✓ define ✓ build ✓ prove ✓ polish ✓ review ✓ seal ◉ ship ○
3
+ Flow spec ✓ define ✓ plan ✓ build ✓ prove ✓ polish ✓ review ✓ seal ◉ ship ○
@@ -12,19 +12,8 @@ import (
12
12
  "github.com/devrites/devrites/internal/state"
13
13
  )
14
14
 
15
- // phaseArc is the canonical order the lifecycle advances through (ADR-0004).
16
- var phaseArc = []state.Phase{
17
- state.PhaseFrame,
18
- state.PhaseSpec,
19
- state.PhasePlan,
20
- state.PhaseBuild,
21
- state.PhaseProve,
22
- state.PhaseVet,
23
- state.PhaseSeal,
24
- state.PhaseShip,
25
- }
26
-
27
15
  func TestADR0004RequiredSectionsAreAdditiveDownTheArc(t *testing.T) {
16
+ phaseArc := state.LifecyclePhases()
28
17
  for _, p := range phaseArc {
29
18
  if !state.KnownPhase(p) {
30
19
  t.Fatalf("phase %q in the arc is not a known phase", p)
@@ -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")
@@ -795,7 +795,7 @@ func TestParityPreamble(t *testing.T) {
795
795
  //
796
796
  // Golden: the header rule width, the slice meter (round-half math, ✅ ALL BUILT
797
797
  // vs the last-built tail), and the flow ribbon (conditional temper/vet, the
798
- // plan→build and done cursor rules) are checked against golden snapshots.
798
+ // explicit plan and terminal done cursor rules) are checked against golden snapshots.
799
799
 
800
800
  func TestParityProgress(t *testing.T) {
801
801
  work := t.TempDir()
@@ -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
  }
@@ -9,9 +9,11 @@ workspace map; `proof.md` may stand in for `evidence.md`.
9
9
 
10
10
  | Phase | Required artifacts |
11
11
  | --- | --- |
12
+ | frame | `state.md` |
12
13
  | spec | `README.md`/`index.md`/`feature.md`, `brief.md`, `spec.md`, `state.md`, `decisions.md`, `assumptions.md`, `questions.md` |
13
- | define/plan/vet/build | spec artifacts plus `architecture.md`, `plan.md`, `tasks.md`, `traceability.md` |
14
- | prove/polish/review/seal/ship | plan artifacts plus `evidence.md`/`proof.md`, `touched-files.md` |
14
+ | temper | spec artifacts |
15
+ | define/plan/vet/build/converge | spec artifacts plus `architecture.md`, `plan.md`, `tasks.md`, `traceability.md` |
16
+ | prove/polish/review/seal/ship/done | plan artifacts plus `evidence.md`/`proof.md`, `touched-files.md` |
15
17
  | conditional | `flows.md` when diagrams clarify; `design-brief.md` and `browser-evidence.md` for UI; `drift.md` for drift; `handoff.md` only when requested; `references.md` + `references/` when references exist |
16
18
 
17
19
  ## What each file owns
@@ -33,7 +35,7 @@ workspace map; `proof.md` may stand in for `evidence.md`.
33
35
  | `evidence.md` / `proof.md` | `EVID-###` command/action, result, timestamp if available, related AC/slice IDs, limitation | 280 lines |
34
36
  | `browser-evidence.md` | UI route/viewports/screenshots/console/network/interactions and Visual Verdict | 220 lines |
35
37
  | `drift.md` | `DRIFT-###` spec/plan drift and resolution | 160 lines |
36
- | `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review | 180 lines |
38
+ | `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review | 160 lines |
37
39
  | `design-brief.md` | UI design direction, states, interaction model | 160 lines |
38
40
  | `handoff.md` | cold-resume guide: current objective, last completed slice, next action, blockers, read-next links | 120 lines |
39
41