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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [3.0.3](https://github.com/ViktorsBaikers/DevRites/compare/v3.0.2...v3.0.3) (2026-07-20)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **rite:** centralize workflow schema ([58bd55e](https://github.com/ViktorsBaikers/DevRites/commit/58bd55e8472a98be1073bcfa5fe7593e7bf0c5d1))
|
|
10
|
+
|
|
11
|
+
## [3.0.2](https://github.com/ViktorsBaikers/DevRites/compare/v3.0.1...v3.0.2) (2026-07-20)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **rite:** read canonical workspace cursor state ([e2fd60c](https://github.com/ViktorsBaikers/DevRites/commit/e2fd60c13aa53637e1af02a792f36f3efe32e1c9))
|
|
16
|
+
|
|
5
17
|
## [3.0.1](https://github.com/ViktorsBaikers/DevRites/compare/v3.0.0...v3.0.1) (2026-07-20)
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ Full diagram set (lifecycle, polish orchestrator, review fan-out, debug loop,
|
|
|
115
115
|
rules carrier, workspace state, namespace map) →
|
|
116
116
|
[`docs/flow.md`](docs/flow.md).
|
|
117
117
|
|
|
118
|
-
**Status:** [`v3.0.
|
|
118
|
+
**Status:** [`v3.0.3`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.0.3) — see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
119
119
|
|
|
120
120
|
## Contents
|
|
121
121
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ADR-0004: Phase-relative section completeness
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-07-08 (backfilled)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
A feature's working state must be legible to both humans and the engine, and
|
|
9
|
+
"is it complete?" must be answerable deterministically at each step of the
|
|
10
|
+
lifecycle. A single long document makes completeness opaque and burns context;
|
|
11
|
+
a rigid "all fields required always" model blocks early phases on artifacts
|
|
12
|
+
that don't exist yet (there's no proof during framing).
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
Model feature state as **six single-concern section files** — `spec`, `plan`,
|
|
17
|
+
`decisions`, `tasks`, `proof`, `status` (with transitional aliases
|
|
18
|
+
`evidence→proof`, `state→status` that `migrate` normalizes). Drive the lifecycle
|
|
19
|
+
through the ordered rite-\* arc:
|
|
20
|
+
`frame → spec → temper → define → plan → vet → build → converge → prove → polish → review → seal → ship → done`.
|
|
21
|
+
Completeness is **phase-relative and additive**: the typed phase registry maps
|
|
22
|
+
each phase to the sections that must have real content to leave it; a section
|
|
23
|
+
not yet required never blocks. `SchemaVersion = 1`; a feature may declare its
|
|
24
|
+
own and the engine refuses anything newer than it understands. Files evolve
|
|
25
|
+
additively.
|
|
26
|
+
|
|
27
|
+
## Alternatives considered
|
|
28
|
+
|
|
29
|
+
| Option | Why not |
|
|
30
|
+
|--------|---------|
|
|
31
|
+
| One `feature.md` mega-document | Completeness isn't self-evident; every read pays for the whole file. |
|
|
32
|
+
| All sections required in every phase | Blocks framing/spec phases on proof/tasks that legitimately don't exist yet. |
|
|
33
|
+
| Status inferred from git / external tracker | Couples state to a tool outside the workspace; breaks the git-diffable, self-contained record. |
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
|
|
37
|
+
- Completeness is a table lookup, not a judgment call — cheap and reproducible.
|
|
38
|
+
- Small files stay context-cheap and make "what's missing" self-evident.
|
|
39
|
+
- `SchemaVersion = 1` is young; migration is single-version. Hardening the
|
|
40
|
+
writer into one pure transition function is a recorded follow-up
|
|
41
|
+
(`docs/research/gsd-core-adoption.md` §3.2).
|
|
42
|
+
- The typed registry in `engine/internal/state/schema.go` is the lifecycle
|
|
43
|
+
authority. Its invariant tests lock order, aliases, commands, requirements,
|
|
44
|
+
and cross-format manifest freshness.
|
package/docs/command-map.md
CHANGED
|
@@ -167,7 +167,7 @@ when available, falling back to file reads otherwise:
|
|
|
167
167
|
See [`flow.md`](flow.md) for the Mermaid diagrams. The text path:
|
|
168
168
|
|
|
169
169
|
```
|
|
170
|
-
/rite-spec → /rite-define → /rite-build ×N → /rite-prove → /rite-polish → /rite-review → /rite-seal → /rite-ship
|
|
170
|
+
/rite-frame → /rite-spec → /rite-temper → /rite-define → /rite-vet → /rite-build ×N → /rite-converge → /rite-prove → /rite-polish → /rite-review → /rite-seal → /rite-ship
|
|
171
171
|
│ │ │ ▲ │ │ (decide) (execute+close)
|
|
172
172
|
│ │ │ └ Spec Drift Guard → /rite-plan repair ────┘
|
|
173
173
|
│ │ └ devrites-frontend-craft / source-driven / doubt
|
|
@@ -65,8 +65,9 @@ The per-feature index. Its YAML frontmatter carries:
|
|
|
65
65
|
| `phase` | current workflow phase (see below) |
|
|
66
66
|
| `schemaVersion` | schema version the file was written against |
|
|
67
67
|
|
|
68
|
-
A feature exists
|
|
69
|
-
|
|
68
|
+
A feature exists when it has either a live `state.md` ledger or the transitional
|
|
69
|
+
`feature.md` manifest. The mutable `state.md` cursor is authoritative when both
|
|
70
|
+
declare a phase; an unknown declared phase is an error.
|
|
70
71
|
|
|
71
72
|
## Sections
|
|
72
73
|
|
|
@@ -91,16 +92,18 @@ requires only the sections needed to leave it, and the set grows additively down
|
|
|
91
92
|
the arc. A section that is not yet required (e.g. `proof` during the `spec`
|
|
92
93
|
phase) never blocks.
|
|
93
94
|
|
|
94
|
-
| phase
|
|
95
|
-
|
|
|
96
|
-
| `frame` | *(none)*
|
|
97
|
-
| `spec`
|
|
98
|
-
| `plan`
|
|
99
|
-
| `build` | `spec`, `plan`, `decisions`, `tasks`
|
|
100
|
-
| `prove` | `spec`, `plan`, `decisions`, `tasks`, `proof`
|
|
101
|
-
| `
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
| phase | required sections |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `frame` | *(none)* |
|
|
98
|
+
| `spec`, `temper` | `spec` |
|
|
99
|
+
| `define`, `plan` | `spec`, `plan` |
|
|
100
|
+
| `vet`, `build`, `converge` | `spec`, `plan`, `decisions`, `tasks` |
|
|
101
|
+
| `prove`, `polish`, `review` | `spec`, `plan`, `decisions`, `tasks`, `proof` |
|
|
102
|
+
| `seal`, `ship`, `done` | `spec`, `plan`, `decisions`, `tasks`, `proof`, `status` |
|
|
103
|
+
|
|
104
|
+
The authoritative typed definitions live in `engine/internal/state/schema.go`.
|
|
105
|
+
`workflow_manifest.json` is a generated derivative for non-Go release tools;
|
|
106
|
+
run `go generate ./internal/state` after editing the registry.
|
|
104
107
|
|
|
105
108
|
## `devrites-engine status <slug>`
|
|
106
109
|
|
package/docs/flow.md
CHANGED
|
@@ -270,7 +270,7 @@ erDiagram
|
|
|
270
270
|
string slug PK ".devrites/work/<slug>/"
|
|
271
271
|
}
|
|
272
272
|
state {
|
|
273
|
-
string phase "spec | plan | build | prove | polish | review | seal | ship | done"
|
|
273
|
+
string phase "frame | spec | temper | define | plan | vet | build | converge | prove | polish | review | seal | ship | done"
|
|
274
274
|
string status "running | awaiting_human | blocked | done"
|
|
275
275
|
string active_slice "N — name"
|
|
276
276
|
int afk_slices_remaining "from .devrites/AFK max_slices on first AFK build"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Go Authoritative Workflow Schema and Quality Research
|
|
2
|
+
|
|
3
|
+
Date: 2026-07-20
|
|
4
|
+
|
|
5
|
+
## Question
|
|
6
|
+
|
|
7
|
+
How should a Go CLI keep lifecycle and schema values authoritative, avoid
|
|
8
|
+
shotgun changes, decide between code and configuration, safely generate
|
|
9
|
+
derivatives, validate the model, and find dead or defective code?
|
|
10
|
+
|
|
11
|
+
This note uses primary or first-party technical sources. Recommendations marked
|
|
12
|
+
**Inference for DevRites** apply those sources to this repository.
|
|
13
|
+
|
|
14
|
+
## Executive recommendation
|
|
15
|
+
|
|
16
|
+
Use one typed, ordered Go registry as the authority for machine-readable
|
|
17
|
+
workflow facts. A phase definition should own its stable identifier, order,
|
|
18
|
+
required sections, aliases, and other phase-level policy. Consumers should ask
|
|
19
|
+
the registry instead of maintaining their own phase slices, maps, or switches.
|
|
20
|
+
|
|
21
|
+
Keep the registry in code, not runtime configuration: the lifecycle is versioned
|
|
22
|
+
application behavior and should change with tests and a release. Generate files
|
|
23
|
+
only where a second representation is genuinely required. Validate registry
|
|
24
|
+
invariants in one test and make regeneration drift a CI failure.
|
|
25
|
+
|
|
26
|
+
## 1. Model stable domain values as typed code
|
|
27
|
+
|
|
28
|
+
Go supports defined string/numeric types with methods, typed constants, and
|
|
29
|
+
related constant sets. The language specification explicitly shows defined
|
|
30
|
+
types combined with constants and methods; `iota` is available for sequential
|
|
31
|
+
integer sets ([Go specification: type definitions and constants](https://go.dev/ref/spec#Type_definitions),
|
|
32
|
+
[Go specification: `iota`](https://go.dev/ref/spec#Iota)). Protocol Buffers uses
|
|
33
|
+
the same broad shape when generating Go enums: a defined type, typed constants,
|
|
34
|
+
a `String` method, and lookup metadata derived from the schema
|
|
35
|
+
([Go generated code guide: enumerations](https://protobuf.dev/reference/go/go-generated/#enumerations)).
|
|
36
|
+
|
|
37
|
+
**Inference for DevRites:** retain `type Phase string` because serialized state
|
|
38
|
+
needs stable readable values, but make one ordered `[]PhaseDefinition` the
|
|
39
|
+
machine authority. Keep named `Phase...` constants so callers remain type-safe;
|
|
40
|
+
declare each serialized string only once. Derive or query all of the following
|
|
41
|
+
from the registry:
|
|
42
|
+
|
|
43
|
+
- known-phase validation;
|
|
44
|
+
- lifecycle order and progress position;
|
|
45
|
+
- required sections and proof/status requirements;
|
|
46
|
+
- legacy aliases used by migration;
|
|
47
|
+
- phase groups such as pre-build, proof-required, and terminal;
|
|
48
|
+
- user-facing labels where labels are truly metadata.
|
|
49
|
+
|
|
50
|
+
Do not replace several duplicated maps with one giant untyped map. A small
|
|
51
|
+
struct gives each fact a name and lets the compiler check field types. Do not
|
|
52
|
+
put unrelated constants such as filenames, parser field aliases, UI glyphs, and
|
|
53
|
+
timeouts into a global `constants.go`; centralize a fact at the narrowest package
|
|
54
|
+
that owns it.
|
|
55
|
+
|
|
56
|
+
## 2. Code versus configuration
|
|
57
|
+
|
|
58
|
+
The Twelve-Factor definition draws a useful boundary: configuration is what
|
|
59
|
+
varies between deploys, while internal application configuration that does not
|
|
60
|
+
vary between deploys is best kept in code
|
|
61
|
+
([Twelve-Factor App: Config](https://www.12factor.net/config)).
|
|
62
|
+
|
|
63
|
+
**Inference for DevRites:**
|
|
64
|
+
|
|
65
|
+
| Value | Authority | Reason |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| Phase IDs, order, required artifacts, legal aliases | Typed Go registry | Versioned product semantics; must change with tests and release |
|
|
68
|
+
| Workspace state such as current phase and next action | Validated workspace data | Per-workspace mutable state |
|
|
69
|
+
| Credentials, external service endpoints, machine-specific paths | Environment/CLI config | Deployment or machine variation |
|
|
70
|
+
| User policy that is intentionally supported as an override | Explicit config with defaults and validation | Product feature, not an accidental escape hatch |
|
|
71
|
+
|
|
72
|
+
Making the phase arc runtime-configurable would weaken compatibility guarantees
|
|
73
|
+
and move failures from compile/test time to user runtime. Add configurability
|
|
74
|
+
only when users have a real supported need to define a different lifecycle.
|
|
75
|
+
|
|
76
|
+
## 3. Generate derivatives, never another authority
|
|
77
|
+
|
|
78
|
+
The Go tool documents that `go generate` runs explicit author-defined commands,
|
|
79
|
+
is **never** run automatically by `go build` or `go test`, and generated source
|
|
80
|
+
should carry the standard `// Code generated ... DO NOT EDIT.` marker
|
|
81
|
+
([`go generate` command documentation](https://pkg.go.dev/cmd/go#hdr-Generate_Go_files_by_processing_source)).
|
|
82
|
+
The Go team also states that generated, tested source needed by clients must be
|
|
83
|
+
checked into the repository
|
|
84
|
+
([Go blog: Generating code](https://go.dev/blog/generate)).
|
|
85
|
+
|
|
86
|
+
**Inference for DevRites:** start without a generator when all engine consumers
|
|
87
|
+
can import/query the Go registry. Add a minimal stdlib generator only for
|
|
88
|
+
representations that cannot consume Go directly, such as a compact machine
|
|
89
|
+
manifest shipped in the skills pack or a lifecycle table in documentation.
|
|
90
|
+
|
|
91
|
+
If generation is added:
|
|
92
|
+
|
|
93
|
+
1. The generator reads/imports the authoritative registry; it must not parse a
|
|
94
|
+
copied list.
|
|
95
|
+
2. Output is deterministic, formatted, marked generated, and checked in when
|
|
96
|
+
release consumers need it.
|
|
97
|
+
3. CI runs generation and fails when `git diff --exit-code` is non-zero. This is
|
|
98
|
+
necessary because Go deliberately does not run generators during tests or
|
|
99
|
+
builds.
|
|
100
|
+
4. Humans edit only the registry. Generated files say how to regenerate.
|
|
101
|
+
|
|
102
|
+
Protobuf demonstrates when a separate schema language is justified: one schema
|
|
103
|
+
generates typed code and lookup maps for multiple representations/languages
|
|
104
|
+
([Protocol Buffers Go generated-code guide](https://protobuf.dev/reference/go/go-generated/)).
|
|
105
|
+
DevRites does not need Protobuf, CUE, or JSON Schema merely to centralize a Go
|
|
106
|
+
enum. Consider one only if the lifecycle becomes a cross-language public data
|
|
107
|
+
contract. JSON Schema is specifically a language for annotating and validating
|
|
108
|
+
JSON structure and constraints
|
|
109
|
+
([JSON Schema specification](https://json-schema.org/specification)).
|
|
110
|
+
|
|
111
|
+
## 4. Validate the authority, not every consumer independently
|
|
112
|
+
|
|
113
|
+
The official Go testing guidance recommends table-driven tests when many cases
|
|
114
|
+
share the same logic, avoiding copy-pasted tests
|
|
115
|
+
([Go Wiki: Table-driven tests](https://go.dev/wiki/TableDrivenTests)). Go fuzzing
|
|
116
|
+
is built into the toolchain and is intended to find edge cases humans miss
|
|
117
|
+
([Go fuzzing documentation](https://go.dev/doc/security/fuzz/)).
|
|
118
|
+
|
|
119
|
+
**Inference for DevRites:** add one registry-invariant test that iterates every
|
|
120
|
+
definition and fails on:
|
|
121
|
+
|
|
122
|
+
- an empty or duplicate phase ID;
|
|
123
|
+
- an empty or duplicate alias;
|
|
124
|
+
- duplicate or non-monotonic order;
|
|
125
|
+
- an unknown required section;
|
|
126
|
+
- a transition/group reference to an unknown phase;
|
|
127
|
+
- a terminal phase with a successor;
|
|
128
|
+
- a phase that should require proof/status but whose metadata says otherwise.
|
|
129
|
+
|
|
130
|
+
Then keep small behavior tests at public boundaries: readiness, migration,
|
|
131
|
+
progress, stop gates, and snapshots. These should consume the registry rather
|
|
132
|
+
than restating the whole lifecycle as expected data. A single explicit expected
|
|
133
|
+
phase list is still useful as a contract test; duplication in a test oracle is
|
|
134
|
+
intentional because deriving both input and expected output from the same table
|
|
135
|
+
cannot detect an accidentally missing phase.
|
|
136
|
+
|
|
137
|
+
Fuzz the untrusted text boundary (cursor/state parsing), not the static registry.
|
|
138
|
+
Useful properties are: parsing never panics, unknown phases remain errors,
|
|
139
|
+
canonical writes round-trip, and legacy aliases normalize predictably.
|
|
140
|
+
|
|
141
|
+
## 5. Prevent shotgun changes
|
|
142
|
+
|
|
143
|
+
Centralization works only if consumers cannot quietly recreate the same facts.
|
|
144
|
+
The minimum enforcement is architectural rather than magical:
|
|
145
|
+
|
|
146
|
+
1. Put the registry and queries in the owning `state` package.
|
|
147
|
+
2. Export query functions or read-only copies, not mutable global maps/slices.
|
|
148
|
+
3. Replace lifecycle switches and local `[]Phase` values with registry queries.
|
|
149
|
+
4. Allow consumer switches only for genuinely phase-specific behavior that
|
|
150
|
+
cannot be represented as metadata; require a default that rejects unknown
|
|
151
|
+
phases.
|
|
152
|
+
5. Search CI or a focused test for duplicated full lifecycle literals in code
|
|
153
|
+
and machine templates. Do not ban phase words in prose; documentation must be
|
|
154
|
+
readable and is not executable policy.
|
|
155
|
+
|
|
156
|
+
This is deliberately smaller than building a generic workflow framework. One
|
|
157
|
+
data table plus a few queries solves the observed change-amplification problem.
|
|
158
|
+
|
|
159
|
+
## 6. Static analysis and dead-code audit stack
|
|
160
|
+
|
|
161
|
+
No single tool proves correctness. The Go `vet` documentation says it finds
|
|
162
|
+
suspicious constructs missed by the compiler but uses heuristics and is not a
|
|
163
|
+
firm correctness indicator ([`go vet` documentation](https://pkg.go.dev/cmd/vet)).
|
|
164
|
+
The Go team's `deadcode` command builds a whole-program call graph and reports
|
|
165
|
+
unreachable functions. Its `-test` flag includes test executables, and its docs
|
|
166
|
+
warn that results are specific to one `GOOS`/`GOARCH`/build-tag configuration
|
|
167
|
+
([`deadcode` documentation](https://pkg.go.dev/golang.org/x/tools/cmd/deadcode),
|
|
168
|
+
[Go blog: Finding unreachable functions with deadcode](https://go.dev/blog/deadcode)).
|
|
169
|
+
Staticcheck provides additional correctness and unused-code checks, including
|
|
170
|
+
the `U1000` family ([Staticcheck checks](https://staticcheck.dev/docs/checks/)).
|
|
171
|
+
`govulncheck` reports known vulnerabilities that are actually reachable through
|
|
172
|
+
the program's call graph ([Go vulnerability management](https://go.dev/doc/security/vuln/)).
|
|
173
|
+
|
|
174
|
+
**Recommended audit order:**
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
go test ./...
|
|
178
|
+
go vet ./...
|
|
179
|
+
staticcheck ./...
|
|
180
|
+
deadcode -test ./...
|
|
181
|
+
govulncheck ./...
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Run race-enabled tests for concurrency changes and a bounded fuzz job for state
|
|
185
|
+
parsers. Run `deadcode` for every supported build configuration—at minimum the
|
|
186
|
+
supported Unix and Windows targets—before deleting reported code. Review each
|
|
187
|
+
finding; generated files, reflection, build tags, assembly, and external entry
|
|
188
|
+
points can change reachability assumptions.
|
|
189
|
+
|
|
190
|
+
## 7. Concrete DevRites adoption sequence
|
|
191
|
+
|
|
192
|
+
1. Characterize current behavior with the existing test suite.
|
|
193
|
+
2. Introduce `PhaseDefinition` and the single ordered registry in
|
|
194
|
+
`engine/internal/state`; keep serialized phase strings stable.
|
|
195
|
+
3. Move `KnownPhase`, order, requirements, aliases, groups, and labels behind
|
|
196
|
+
registry queries.
|
|
197
|
+
4. Migrate consumers subsystem by subsystem and delete their local lifecycle
|
|
198
|
+
tables in the same change.
|
|
199
|
+
5. Add the invariant/contract tests and a generated-drift check only if a
|
|
200
|
+
generator is actually introduced.
|
|
201
|
+
6. Run the audit stack, delete only confirmed dead code, and rerun all supported
|
|
202
|
+
platform tests.
|
|
203
|
+
|
|
204
|
+
## Decision summary
|
|
205
|
+
|
|
206
|
+
- **Choose:** one typed Go metadata registry, narrow query API, validation test.
|
|
207
|
+
- **Do not choose yet:** runtime-configurable lifecycle, a general workflow DSL,
|
|
208
|
+
or a new schema/code-generation dependency.
|
|
209
|
+
- **Generate only:** unavoidable cross-format derivatives, with deterministic
|
|
210
|
+
output and CI drift detection.
|
|
211
|
+
- **Audit continuously:** compiler/tests, vet, Staticcheck, deadcode by supported
|
|
212
|
+
build configuration, govulncheck, and focused fuzzing.
|
|
@@ -26,6 +26,21 @@ func makeHookWorkspace(t *testing.T) string {
|
|
|
26
26
|
return root
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
func TestHookStatuslineReadsCanonicalCursor(t *testing.T) {
|
|
30
|
+
root := makeHookWorkspace(t)
|
|
31
|
+
state := "| Key | Value |\n| --- | --- |\n| phase | temper |\n| status | running |\n"
|
|
32
|
+
if err := os.WriteFile(filepath.Join(root, "work", "demo", "state.md"), []byte(state), 0o644); err != nil {
|
|
33
|
+
t.Fatal(err)
|
|
34
|
+
}
|
|
35
|
+
var stdout bytes.Buffer
|
|
36
|
+
if code := hookStatusline(strings.NewReader(""), &stdout, &bytes.Buffer{}); code != 0 {
|
|
37
|
+
t.Fatalf("hookStatusline code=%d", code)
|
|
38
|
+
}
|
|
39
|
+
if !strings.Contains(stdout.String(), "demo · temper ·") {
|
|
40
|
+
t.Fatalf("statusline ignored table phase: %s", stdout.String())
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
func TestHookEventWritesTimelineAndWorkspaceEvents(t *testing.T) {
|
|
30
45
|
root := makeHookWorkspace(t)
|
|
31
46
|
if code := hookEvent([]string{"subagent-stop"}, strings.NewReader(`{}`), &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
|
|
@@ -22,20 +22,8 @@ import (
|
|
|
22
22
|
"github.com/devrites/devrites/internal/state"
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
// State-field patterns. Each hook reads a field the way the scripts do:
|
|
26
|
-
// `grep -iE <find>` (case-INSENSITIVE) selects the first matching line, then
|
|
27
|
-
// `sed -E s/<strip>//` (case-SENSITIVE, as sed is) removes the key prefix — a
|
|
28
|
-
// no-op when the case differs, so the mismatched-case behavior is preserved.
|
|
29
25
|
var (
|
|
30
|
-
|
|
31
|
-
stripNextStep = regexp.MustCompile(`^[-[:space:]]*Next step:[[:space:]]*`)
|
|
32
|
-
findStatus = regexp.MustCompile(`(?i)^[-[:space:]]*Status:`)
|
|
33
|
-
stripStatus = regexp.MustCompile(`^[-[:space:]]*Status:[[:space:]]*`)
|
|
34
|
-
findPhase = regexp.MustCompile(`(?i)^[-[:space:]]*Phase:`)
|
|
35
|
-
stripPhase = regexp.MustCompile(`^[-[:space:]]*Phase:[[:space:]]*`)
|
|
36
|
-
findAFKRem = regexp.MustCompile(`(?i)AFK slices remaining:`)
|
|
37
|
-
stripAFKRem = regexp.MustCompile(`.*remaining:[[:space:]]*`)
|
|
38
|
-
openStatusRe = regexp.MustCompile(`(?i)^[[:space:]]*status:[[:space:]]*open`)
|
|
26
|
+
openStatusRe = regexp.MustCompile(`(?i)^[[:space:]]*status:[[:space:]]*open`)
|
|
39
27
|
)
|
|
40
28
|
|
|
41
29
|
// resolveWorkspaceDir resolves the active feature's directory. New writes default
|
|
@@ -199,9 +187,9 @@ func hookHandoffSnapshot(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
199
187
|
return exitOK
|
|
200
188
|
}
|
|
201
189
|
stateLines := wsReadLines(filepath.Join(dir, "state.md"))
|
|
202
|
-
phase :=
|
|
203
|
-
status :=
|
|
204
|
-
next :=
|
|
190
|
+
phase, _ := state.CursorField(stateLines, state.CursorPhase)
|
|
191
|
+
status, _ := state.CursorField(stateLines, state.CursorStatus)
|
|
192
|
+
next, _ := state.CursorField(stateLines, state.CursorNextAction)
|
|
205
193
|
stamp := time.Now().UTC().Format(time.RFC3339)
|
|
206
194
|
var b strings.Builder
|
|
207
195
|
fmt.Fprintf(&b, "\n## Handoff snapshot — %s\n", stamp)
|
|
@@ -233,12 +221,12 @@ func hookCursor(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
233
221
|
}
|
|
234
222
|
stateLines := wsReadLines(filepath.Join(dir, "state.md"))
|
|
235
223
|
|
|
236
|
-
next :=
|
|
237
|
-
status :=
|
|
224
|
+
next, _ := state.CursorField(stateLines, state.CursorNextAction)
|
|
225
|
+
status, _ := state.CursorField(stateLines, state.CursorStatus)
|
|
238
226
|
gates := wsGateCount(filepath.Join(dir, "questions.md"))
|
|
239
227
|
afk := ""
|
|
240
228
|
if wsIsFile(filepath.Join(root, "AFK")) {
|
|
241
|
-
afk =
|
|
229
|
+
afk, _ = state.CursorField(stateLines, state.CursorAFKSlicesRemaining)
|
|
242
230
|
}
|
|
243
231
|
|
|
244
232
|
fmt.Fprintf(stdout, "DevRites cursor — active feature: %s\n", slug)
|
|
@@ -267,7 +255,7 @@ func hookStatusline(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
267
255
|
if !ok {
|
|
268
256
|
return exitOK
|
|
269
257
|
}
|
|
270
|
-
phase :=
|
|
258
|
+
phase, _ := state.CursorField(wsReadLines(filepath.Join(dir, "state.md")), state.CursorPhase)
|
|
271
259
|
if phase == "" {
|
|
272
260
|
phase = "?"
|
|
273
261
|
}
|
|
@@ -536,17 +524,6 @@ func hookWrightScope(h harness.Harness, stdin io.Reader, stdout, stderr io.Write
|
|
|
536
524
|
return exitOK
|
|
537
525
|
}
|
|
538
526
|
|
|
539
|
-
// wsField mirrors `grep -iE <find> | head -1 | sed -E s/<strip>//`: the first line
|
|
540
|
-
// matching find (case-insensitive) with strip removed (case-sensitive).
|
|
541
|
-
func wsField(lines []string, find, strip *regexp.Regexp) string {
|
|
542
|
-
for _, line := range lines {
|
|
543
|
-
if find.MatchString(line) {
|
|
544
|
-
return strip.ReplaceAllString(line, "")
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return ""
|
|
548
|
-
}
|
|
549
|
-
|
|
550
527
|
// wsGateCount mirrors `grep -ciE '^\s*status:\s*open' questions.md || echo 0`,
|
|
551
528
|
// INCLUDING grep's quirk: on a present file with zero matches grep prints "0" and
|
|
552
529
|
// exits 1, so `|| echo 0` appends a second "0" (the captured value is "0\n0").
|
|
@@ -90,7 +90,7 @@ func (r *Result) Render() string {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// StopGate evaluates the stop-hook rest-point invariant for the active feature:
|
|
93
|
-
// a feature that CLAIMS completion (it has advanced to
|
|
93
|
+
// a feature that CLAIMS completion (it has advanced to seal, ship, or done)
|
|
94
94
|
// must not have an empty proof section. This is a rest-point check, NOT
|
|
95
95
|
// whole-feature completeness — normal in-progress incompleteness never trips it,
|
|
96
96
|
// so a mid-build turn is never blocked.
|
|
@@ -129,7 +129,7 @@ func StopGate(root string) (StopResult, error) {
|
|
|
129
129
|
slug, strings.Join(gates, "/")),
|
|
130
130
|
}, nil
|
|
131
131
|
}
|
|
132
|
-
claimsDone :=
|
|
132
|
+
claimsDone := state.ShippablePhase(f.Phase)
|
|
133
133
|
if claimsDone && !f.Present[state.SectionProof] {
|
|
134
134
|
return StopResult{
|
|
135
135
|
Slug: slug,
|
|
@@ -167,14 +167,14 @@ func openBlockingQuestionGates(data []byte) []string {
|
|
|
167
167
|
inQ := false
|
|
168
168
|
status, gate := "", ""
|
|
169
169
|
finalize := func() {
|
|
170
|
-
if inQ && status == "open" && (gate == "blocking" || gate == "validating") && !seen[gate] {
|
|
170
|
+
if inQ && status == "open" && (gate == "blocking" || gate == "validating" || gate == "escalating") && !seen[gate] {
|
|
171
171
|
seen[gate] = true
|
|
172
172
|
gates = append(gates, gate)
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
for _, line := range lines {
|
|
176
176
|
switch {
|
|
177
|
-
case strings.HasPrefix(line, "## q-"):
|
|
177
|
+
case strings.HasPrefix(strings.ToLower(line), "## q-"):
|
|
178
178
|
finalize()
|
|
179
179
|
inQ, status, gate = true, "", ""
|
|
180
180
|
case inQ && strings.HasPrefix(line, "status:"):
|
|
@@ -191,13 +191,8 @@ func openBlockingQuestionGates(data []byte) []string {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
func stateAwaitingHuman(data []byte) bool {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
status := strings.TrimLeft(strings.TrimPrefix(line, "- Status:"), gateSpaceChars)
|
|
197
|
-
return status == "awaiting_human"
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return false
|
|
194
|
+
status, _ := state.CursorField(splitLinesNoTrailing(data), state.CursorStatus)
|
|
195
|
+
return status == "awaiting_human"
|
|
201
196
|
}
|
|
202
197
|
|
|
203
198
|
func isHeadingLine(line string) bool {
|
|
@@ -38,6 +38,13 @@ func TestCheckAndRenderReadiness(t *testing.T) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
func TestStateAwaitingHumanReadsCanonicalCursor(t *testing.T) {
|
|
42
|
+
data := []byte("| Key | Value |\n| --- | --- |\n| status | awaiting_human |\n")
|
|
43
|
+
if !stateAwaitingHuman(data) {
|
|
44
|
+
t.Fatal("stateAwaitingHuman ignored canonical cursor table")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
func TestStopGateRestPointInvariants(t *testing.T) {
|
|
42
49
|
for _, tc := range []struct {
|
|
43
50
|
name string
|
|
@@ -82,6 +89,15 @@ func TestStopGateRestPointInvariants(t *testing.T) {
|
|
|
82
89
|
wantBlocked: true,
|
|
83
90
|
wantReasonSub: "proof.md is empty",
|
|
84
91
|
},
|
|
92
|
+
{
|
|
93
|
+
name: "done without proof blocks",
|
|
94
|
+
files: map[string]string{
|
|
95
|
+
"feature.md": "---\nphase: ship\nschemaVersion: 1\n---\n",
|
|
96
|
+
"state.md": "| Key | Value |\n| --- | --- |\n| phase | done |\n",
|
|
97
|
+
},
|
|
98
|
+
wantBlocked: true,
|
|
99
|
+
wantReasonSub: "proof.md is empty",
|
|
100
|
+
},
|
|
85
101
|
} {
|
|
86
102
|
t.Run(tc.name, func(t *testing.T) {
|
|
87
103
|
root := t.TempDir()
|
|
@@ -130,8 +146,8 @@ func TestStopGateUsesWorkspaceOverride(t *testing.T) {
|
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
func TestOpenBlockingQuestionGates(t *testing.T) {
|
|
133
|
-
got := openBlockingQuestionGates([]byte("##
|
|
134
|
-
want := []string{"blocking", "validating"}
|
|
149
|
+
got := openBlockingQuestionGates([]byte("## Q-1\nstatus: open\ngate: blocking\n\n## Not a question\n\n## q-2\nstatus: open\ngate: validating\n\n## q-3\nstatus: resolved\ngate: blocking\n\n## q-4\nstatus: open\ngate: blocking\n\n## Q-5\nstatus: open\ngate: escalating\n"))
|
|
150
|
+
want := []string{"blocking", "validating", "escalating"}
|
|
135
151
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
136
152
|
t.Fatalf("openBlockingQuestionGates=%v, want %v", got, want)
|
|
137
153
|
}
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
"regexp"
|
|
8
8
|
"strings"
|
|
9
9
|
|
|
10
|
+
"github.com/devrites/devrites/internal/state"
|
|
10
11
|
"github.com/devrites/devrites/internal/workflow"
|
|
11
12
|
)
|
|
12
13
|
|
|
@@ -67,22 +68,15 @@ func BuildReadiness(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
67
68
|
// state.md field value.
|
|
68
69
|
var fieldAnnotation = regexp.MustCompile(`[[:space:]]*(#|\|).*$`)
|
|
69
70
|
|
|
70
|
-
// readinessField reads
|
|
71
|
-
//
|
|
72
|
-
// first match, or "" when the key is absent. A key may contain spaces (e.g.
|
|
73
|
-
// "Plan approved") — it is matched literally.
|
|
71
|
+
// readinessField reads canonical cursor tables and legacy bullet fields, then
|
|
72
|
+
// trims the legacy trailing annotations accepted by the original command.
|
|
74
73
|
func readinessField(lines []string, key string) string {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
value := line[loc[1]:]
|
|
82
|
-
if m := fieldAnnotation.FindStringIndex(value); m != nil {
|
|
83
|
-
value = value[:m[0]]
|
|
84
|
-
}
|
|
85
|
-
return strings.TrimRight(value, spaceChars)
|
|
74
|
+
value, ok := state.CursorField(lines, key)
|
|
75
|
+
if !ok {
|
|
76
|
+
return ""
|
|
77
|
+
}
|
|
78
|
+
if m := fieldAnnotation.FindStringIndex(value); m != nil {
|
|
79
|
+
value = value[:m[0]]
|
|
86
80
|
}
|
|
87
|
-
return
|
|
81
|
+
return strings.TrimRight(value, spaceChars)
|
|
88
82
|
}
|