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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/docs/adr/0004-state-schema-phases-sections.md +44 -0
- package/docs/command-map.md +1 -1
- package/docs/engine/state-schema.md +15 -12
- package/docs/flow.md +1 -1
- package/docs/research/go-authoritative-workflow-schema-and-quality-2026-07-20.md +212 -0
- package/engine/hooks_events_test.go +15 -0
- package/engine/hooks_workspace.go +8 -31
- package/engine/internal/gate/gate.go +6 -11
- package/engine/internal/gate/gate_test.go +18 -2
- package/engine/internal/lib/buildreadiness.go +10 -16
- package/engine/internal/lib/cursor_compat_test.go +120 -0
- package/engine/internal/lib/preamble.go +1 -1
- package/engine/internal/lib/preamble_questions_test.go +10 -0
- package/engine/internal/lib/progress.go +69 -29
- package/engine/internal/lib/resolve.go +21 -17
- package/engine/internal/lib/resolve_remediation_test.go +9 -0
- package/engine/internal/lib/tickafk.go +11 -28
- package/engine/internal/migrate/migrate.go +13 -32
- package/engine/internal/migrate/migrate_test.go +11 -3
- package/engine/internal/state/cmd/workflowmanifest/main.go +54 -0
- package/engine/internal/state/cursor.go +119 -0
- package/engine/internal/state/cursor_test.go +58 -0
- package/engine/internal/state/feature.go +25 -48
- package/engine/internal/state/schema.go +151 -24
- package/engine/internal/state/snapshot.go +60 -3
- package/engine/internal/state/state_test.go +119 -3
- package/engine/internal/state/workflow_manifest.json +310 -0
- package/engine/internal/workflow/commands.go +10 -29
- package/engine/internal/workflow/commands_test.go +18 -0
- package/engine/testdata/golden/TestParityProgress/arg=allbuilt.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=done.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=mid.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=nophase.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=noslice.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=plan.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=seal.golden +1 -1
- package/engine/tests/adr_0004_required_by_phase_test.go +1 -12
- package/engine/tests/gate_test.go +22 -0
- package/engine/tests/lib_parity_test.go +1 -1
- package/engine/tests/migrate_cli_test.go +3 -3
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
- package/pack/generated/codex/AGENTS.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
- package/package.json +1 -1
- package/scripts/codex-generate.sh +1 -1
- package/scripts/grade-feature.sh +5 -3
- package/scripts/run-outcome-evals.sh +21 -0
- package/scripts/validate-workspace-schema.py +9 -73
- package/scripts/validate.sh +10 -0
- 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
|
|
152
|
-
//
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
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\
|
|
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:
|
|
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:
|
|
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
|
-
|
|
|
14
|
-
|
|
|
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 |
|
|
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
|
|