devrites 6.0.3 → 6.0.4

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 (35) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/engine/commands.md +4 -4
  4. package/engine/internal/lib/context.go +11 -5
  5. package/engine/internal/lib/context_next_test.go +52 -0
  6. package/engine/internal/lib/dispatch.go +44 -5
  7. package/engine/internal/lib/metrics.go +74 -1
  8. package/engine/internal/lib/observe_budgets_test.go +32 -0
  9. package/engine/internal/lib/observe_summary.go +8 -0
  10. package/engine/main.go +1 -1
  11. package/pack/.claude/agents/devrites-slice-wright.md +4 -1
  12. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +3 -1
  13. package/pack/.claude/skills/rite-build/reference/output.md +1 -1
  14. package/pack/.claude/skills/rite-build/reference/phase-contract.md +2 -0
  15. package/pack/generated/claude/agents/devrites-slice-wright.md +4 -1
  16. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +3 -1
  17. package/pack/generated/claude/skills/rite-build/reference/output.md +1 -1
  18. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +2 -0
  19. package/pack/generated/codex/agents/devrites-slice-wright.toml +4 -1
  20. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +3 -1
  21. package/pack/generated/codex/skills/rite-build/reference/output.md +1 -1
  22. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +2 -0
  23. package/pack/generated/devin/agents/devrites-slice-wright.md +4 -1
  24. package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +3 -1
  25. package/pack/generated/devin/skills/rite-build/reference/output.md +1 -1
  26. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +2 -0
  27. package/pack/generated/omp/agents/devrites-slice-wright.md +4 -1
  28. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +3 -1
  29. package/pack/generated/omp/skills/rite-build/reference/output.md +1 -1
  30. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +2 -0
  31. package/pack/generated/pi/agents/devrites-slice-wright.md +4 -1
  32. package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +3 -1
  33. package/pack/generated/pi/skills/rite-build/reference/output.md +1 -1
  34. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +2 -0
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
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
+ ## [6.0.4](https://github.com/ViktorsBaikers/DevRites/compare/v6.0.3...v6.0.4) (2026-10-15)
6
+
7
+ ### Changed
8
+
9
+ * **devrites:** anchor root bundles, wright self-review, per-slice cost ([#105](https://github.com/ViktorsBaikers/DevRites/issues/105)) ([3569ab3](https://github.com/ViktorsBaikers/DevRites/commit/3569ab3d7c313a05c6371c696df9b2b40971ce2b))
10
+
5
11
  ## [6.0.3](https://github.com/ViktorsBaikers/DevRites/compare/v6.0.2...v6.0.3) (2026-10-14)
6
12
 
7
13
  ### Changed
package/README.md CHANGED
@@ -31,7 +31,7 @@ history can't lose it. After you clear the conversation, switch hosts, or hand
31
31
  the feature to another engineer, the next session reads the same plan,
32
32
  decisions, and evidence before it continues.
33
33
 
34
- **Status:** [`v6.0.3`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.0.3): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
34
+ **Status:** [`v6.0.4`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.0.4): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
35
35
 
36
36
  This is the latest published release; `main` may contain unreleased work.
37
37
 
@@ -27,15 +27,15 @@ workflow strategy.
27
27
  | `check diff-scope <slug> --allow <csv>\|--allow-file <path> [--worktree\|--staged\|--base <ref>]` | Verify the changed-path set stays inside the declared allowlist; the mechanical gate run before reviewer dispatch. |
28
28
  | `check skill-trust <path>` | Scan one skill/agent Markdown file for structural trust violations. |
29
29
  | `check indexes [--root <dir>]` | Report install manifest and code-index presence as JSON. |
30
- | `observe summary <slug>` | Emit sanitized JSON workspace summary from one retained observation. `task_graph.ok` is true iff `task_graph.problems` is empty; `problems` lists cycle, unknown-dep, malformed-token, duplicate-id, and missing-`Dependencies` blockers. |
30
+ | `observe summary <slug>` | Emit sanitized JSON workspace summary from one retained observation. `task_graph.ok` is true iff `task_graph.problems` is empty; `problems` lists cycle, unknown-dep, malformed-token, duplicate-id, and missing-`Dependencies` blockers. `inflight_waves` lists dispatch waves still `open` or `sealed`, with their pending roles. |
31
31
  | `orient <slug>` | Alias for `observe summary`. |
32
32
  | `observe slice <slug> <SLICE-ID>` | Print one `SLICE-###` section of `tasks.md` so Build reads a slice, not the file. |
33
33
  | `next [slug]` | Print the minimal remaining lifecycle path plus advisory skips. |
34
34
  | `handoff [slug]` | Emit the deterministic resume record: cursor fields, `awaiting_human`, open `questions.md` gate kinds, the `gates.md` reduction with unmet/stale/abandoned ids, `decisions.md` dead ends, missing required files, and the canonical read-next order. Read-only; the `/rite-handoff` spine and the compaction-fallback entry point. |
35
- | `context [slug] (--phase <p>\|--skill <name>) [--role <r>] [--slice <SLICE-ID>] [--trigger a,b]` | Emit one deduplicated read-set bundle per phase/role/skill from each skill's `loads:` manifest; `--skill` works without a workspace when `--out` is given. A `--role` bundle leads with the role's agent contract and names the manifest's `rootOnly` paths instead of inlining them. `--slice` inlines that slice's `tasks.md` block and turns each over-budget artifact into a by-reference index (path, SHA-256, headings, line hits for every ID the block cites); repeat `--slice` for several slices; an unknown slice exits `3`. |
36
- | `dispatch <slug> <open\|start\|seal\|return\|status\|abandon>` | Launch-wave barrier for parallel dispatch: seal needs a handle per role, return needs seal; start/return auto-record metrics. A build wave with a wright role needs `--slice <SLICE-ID>` per slice, and a slice-bound vet wave takes it too; every 5 rounds of either kind on one slice, `open` exits `3` until `--reslice-review <DRIFT-ID>` names an unused `drift.md` `## <DRIFT-ID>` heading for that slice. |
35
+ | `context [slug] (--phase <p>\|--skill <name>) [--role <r>] [--slice <SLICE-ID>] [--trigger a,b]` | Emit one deduplicated read-set bundle per phase/role/skill from each skill's `loads:` manifest; `--skill` works without a workspace when `--out` is given. A bundle without `--role` (the root's own read) gives over-budget artifacts as by-reference indexes; a `--role` bundle leads with the role's agent contract and names the manifest's `rootOnly` paths instead of inlining them. `--slice` inlines that slice's `tasks.md` block and turns each over-budget artifact into a by-reference index (path, SHA-256, headings, line hits for every ID the block cites); repeat `--slice` for several slices; an unknown slice exits `3`. |
36
+ | `dispatch <slug> <open\|start\|seal\|return\|status\|abandon>` | Launch-wave barrier for parallel dispatch: seal needs a handle per role, return needs seal; start/return auto-record metrics. A build wave with a wright role needs `--slice <SLICE-ID>` per slice, and a slice-bound vet wave takes it too; every 5 rounds of either kind on one slice, `open` exits `3` until `--reslice-review <DRIFT-ID>` names an unused `drift.md` `## <DRIFT-ID>` heading for that slice. `open` also warns when an earlier wave for the same slice never returned. |
37
37
  | `metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]` | Append an event to the workspace's `metrics.jsonl` ledger. |
38
- | `metrics summary [slug]` | Reduce `metrics.jsonl` into a per-phase roll-up. |
38
+ | `metrics summary [slug] [--by-slice]` | Reduce `metrics.jsonl` into a per-phase roll-up. `--by-slice` reads `dispatch.json` instead: per slice, build wright rounds, slice-bound Vet rechecks, and agent-minutes (role start to `returned_at`; abandoned waves excluded). |
39
39
  | `claim <add\|release\|list\|check>` | Advisory session-scoped file claims in `.devrites/claims.jsonl` (append-only). `add` records intent to write a path set with a TTL (1–240 min, default 30); a live claim by another session exits `3` and names the holder. `release` appends a release event owned by the claiming session. `list` shows live claims (`--all` includes expired/released). `check` is the read-only preflight for a path set. Coordination aid only — never a substitute for one-writer-per-worktree. |
40
40
  | `note <add\|list\|check\|rm> <slug> ...` | Anchored workspace notes in `notes.md`: `add <slug> <subject> <quote> <title> [body]` binds a note to a verbatim quote in one repository file and assigns `NOTE-###`; `list` and `check` regrade each anchor `exact`/`moved`/`stale`/`ambiguous`/`lost` by searching the repo for the quote (dependency and workspace trees excluded); `check --repair` rewrites a `moved` subject to its new file; `rm` deletes by ID. `check seal` refuses a malformed `notes.md` and any non-`exact` anchor. |
41
41
  | `gates <scaffold\|status\|run\|reverify\|lint\|attest\|abandon> <slug>` | Operate the `gates.md` acceptance ledger: seed per-AC gates, execute runnable gates bound to `test-plan.md` preflight rows, attest manual gates, and reduce to `all-met`/`not-met`/`handoff`/`malformed`. |
@@ -204,7 +204,10 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
204
204
  }
205
205
  for _, rel := range workspaceList {
206
206
  abs := filepath.Join(featureDir, filepath.FromSlash(rel))
207
- if opts.slice == "" {
207
+ // Role bundles without --slice (full Vet, Review, Seal) read
208
+ // everything; root and --slice bundles take over-budget
209
+ // artifacts by anchor.
210
+ if opts.slice == "" && opts.role != "" {
208
211
  if writeContextEntry(&bundle, abs, "workspace/"+rel, seen, false) {
209
212
  loaded++
210
213
  }
@@ -269,6 +272,8 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
269
272
  fmt.Fprintln(stdout, ")")
270
273
  if opts.slice != "" {
271
274
  fmt.Fprintf(stdout, "scope: slice %s (%d whole, %d by-reference)\n", opts.slice, whole, byReference)
275
+ } else if byReference > 0 {
276
+ fmt.Fprintf(stdout, "scope: root (%d whole, %d by-reference over budget)\n", whole, byReference)
272
277
  }
273
278
  if workspaceScoped {
274
279
  fmt.Fprintf(stdout, "workspace: role-scoped for %s (%d of %d declared artifacts)\n",
@@ -334,16 +339,17 @@ func expandIncludes(abs string, data []byte) []byte {
334
339
  })
335
340
  }
336
341
 
337
- // writeSliceEntry writes one workspace artifact of a --slice bundle: tasks.md
338
- // as the slice block plus a by-reference index, any other canonical artifact
339
- // over its line/byte budget as a by-reference index, everything else whole.
342
+ // writeSliceEntry writes one workspace artifact of a --slice or root bundle:
343
+ // with a slice, tasks.md as the slice block plus a by-reference index; any
344
+ // canonical artifact over its line/byte budget as a by-reference index;
345
+ // everything else whole.
340
346
  func writeSliceEntry(bundle *strings.Builder, project, abs, label, slice, block string, ids []citedID, seen map[string]bool) (loaded, byRef bool) {
341
347
  if seen[abs] {
342
348
  return false, false
343
349
  }
344
350
  data, err := os.ReadFile(abs) // #nosec G304 -- abs is resolved from the validated feature workspace
345
351
  name := filepath.Base(abs)
346
- if err == nil && name == "tasks.md" {
352
+ if err == nil && name == "tasks.md" && slice != "" {
347
353
  seen[abs] = true
348
354
  fmt.Fprintf(bundle, "===== %s (%s block) =====\n%s\n", label, slice, block)
349
355
  } else if status, ok := state.ArtifactBudget(name, data); err != nil || !ok || !status.Over {
@@ -858,3 +858,55 @@ func TestContextRepeatedSliceUnion(t *testing.T) {
858
858
  t.Fatalf("union of slice blocks wrong:\n%s", text)
859
859
  }
860
860
  }
861
+
862
+ func TestContextRootBundleAnchorsOverBudgetArtifacts(t *testing.T) {
863
+ root, featureDir := newWorkspace(t, "build")
864
+ skills := newSkillsRoot(t)
865
+ writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\"],\"workspace\":[\"decisions.md\",\"spec.md\"]} -->\n# build\n")
866
+ writeFile(t, filepath.Join(featureDir, "decisions.md"), "# Decisions\n"+strings.Repeat("filler decision row\n", 250)+"## DEC-010 Use the store\n")
867
+ writeFile(t, filepath.Join(featureDir, "spec.md"), "# spec\nsmall spec body\n")
868
+ run := func(args ...string) string {
869
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
870
+ if code := RunContext(root, append([]string{"feat", "--phase", "build", "--skills-root", skills}, args...), stdout, stderr); code != 0 {
871
+ t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
872
+ }
873
+ return stdout.String()
874
+ }
875
+ out := run()
876
+ if !strings.Contains(out, "scope: root (1 whole, 1 by-reference over budget)") {
877
+ t.Fatalf("root scope line missing:\n%s", out)
878
+ }
879
+ rootBundle, _ := os.ReadFile(filepath.Join(featureDir, "ctx", "build.bundle.md"))
880
+ if text := string(rootBundle); !strings.Contains(text, "workspace/decisions.md (by reference)") || strings.Contains(text, "filler decision row") || !strings.Contains(text, "small spec body") {
881
+ t.Fatalf("root bundle must anchor only the over-budget artifact:\n%s", text)
882
+ }
883
+ run("--role", "code-reviewer")
884
+ roleBundle, _ := os.ReadFile(filepath.Join(featureDir, "ctx", "build-code-reviewer.bundle.md"))
885
+ if !strings.Contains(string(roleBundle), "filler decision row") {
886
+ t.Fatalf("a role bundle without --slice must stay whole:\n%s", roleBundle)
887
+ }
888
+ }
889
+
890
+ func TestMetricsSummaryBySlice(t *testing.T) {
891
+ root, featureDir := newWorkspace(t, "build")
892
+ writeFile(t, filepath.Join(featureDir, "dispatch.json"), `{"waves":{
893
+ "w1":{"phase":"build","state":"complete","slices":["SLICE-001"],"roles":{"slice-wright":{"handle":"a","started":"2026-10-15T10:00:00Z","returned":true,"returned_at":"2026-10-15T10:30:00Z"}}},
894
+ "w2":{"phase":"build","state":"complete","slices":["SLICE-001","SLICE-002"],"roles":{"slice-wright":{"handle":"b","started":"2026-10-15T11:00:00Z","returned":true,"returned_at":"2026-10-15T11:20:00Z"}}},
895
+ "w3":{"phase":"build","state":"abandoned","slices":["SLICE-001"],"roles":{"slice-wright":{"handle":"c"}}},
896
+ "v1":{"phase":"vet","state":"complete","slices":["SLICE-002"],"roles":{"plan-reviewer":{"handle":"d","started":"2026-10-15T12:00:00Z","returned":true}}},
897
+ "f1":{"phase":"vet","state":"complete","roles":{"plan-reviewer":{"handle":"e"}}}
898
+ }}`)
899
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
900
+ if code := RunMetrics(root, []string{"summary", "feat", "--by-slice"}, stdout, stderr); code != 0 {
901
+ t.Fatalf("code=%d\n%s", code, stderr)
902
+ }
903
+ out := stdout.String()
904
+ for _, want := range []string{
905
+ "metrics: SLICE-001 wright_rounds=2 vet_rechecks=0 agent_minutes=40\n",
906
+ "metrics: SLICE-002 wright_rounds=1 vet_rechecks=1 agent_minutes=10\n",
907
+ } {
908
+ if !strings.Contains(out, want) {
909
+ t.Fatalf("missing %q:\n%s", want, out)
910
+ }
911
+ }
912
+ }
@@ -56,9 +56,10 @@ type dispatchWave struct {
56
56
  }
57
57
 
58
58
  type dispatchRole struct {
59
- Handle string `json:"handle,omitempty"`
60
- StartedAt string `json:"started,omitempty"`
61
- Returned bool `json:"returned,omitempty"`
59
+ Handle string `json:"handle,omitempty"`
60
+ StartedAt string `json:"started,omitempty"`
61
+ Returned bool `json:"returned,omitempty"`
62
+ ReturnedAt string `json:"returned_at,omitempty"`
62
63
  }
63
64
 
64
65
  func dispatchPath(root, slug string) string {
@@ -270,7 +271,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
270
271
  if r.Returned {
271
272
  return fail("role %q already returned", role)
272
273
  }
273
- r.Returned = true
274
+ r.Returned, r.ReturnedAt = true, nowStamp()
274
275
  all := true
275
276
  for _, o := range w.Roles {
276
277
  if !o.Returned {
@@ -400,12 +401,16 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
400
401
  var lines, overCap []string
401
402
  for _, id := range w.Slices {
402
403
  prior, reviews := 0, 0
403
- for _, other := range f.Waves {
404
+ var unreturned []string
405
+ for name, other := range f.Waves {
404
406
  if other.State == "abandoned" || roundKind(other) != kind {
405
407
  continue
406
408
  }
407
409
  if slices.Contains(other.Slices, id) {
408
410
  prior++
411
+ if other.State == "open" || other.State == "sealed" {
412
+ unreturned = append(unreturned, name)
413
+ }
409
414
  }
410
415
  if slices.Contains(other.ResliceReviewed, id) {
411
416
  reviews++
@@ -416,6 +421,10 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
416
421
  overCap = append(overCap, id)
417
422
  }
418
423
  lines = append(lines, fmt.Sprintf("dispatch: %s %s round %d (reslice review every %d)", id, kind, round, wrightRoundsPerReview))
424
+ if len(unreturned) > 0 {
425
+ sort.Strings(unreturned)
426
+ lines = append(lines, fmt.Sprintf("dispatch: WARNING: %s earlier wave(s) %s never returned; record `dispatch return` or `dispatch abandon --reason`", id, strings.Join(unreturned, ",")))
427
+ }
419
428
  }
420
429
  if len(overCap) == 0 {
421
430
  if review != "" {
@@ -457,6 +466,36 @@ func driftHeading(drift, id string) string {
457
466
  return re.FindString(drift)
458
467
  }
459
468
 
469
+ // InflightWave is a dispatch wave whose agents have not all returned.
470
+ type InflightWave struct {
471
+ Wave string `json:"wave"`
472
+ Phase string `json:"phase"`
473
+ State string `json:"state"`
474
+ Pending []string `json:"pending"`
475
+ Slices []string `json:"slices,omitempty"`
476
+ }
477
+
478
+ // inflightWaves lists open or sealed waves with the roles still awaiting a
479
+ // return, sorted by wave name.
480
+ func inflightWaves(f *dispatchFile) []InflightWave {
481
+ var out []InflightWave
482
+ for name, w := range f.Waves {
483
+ if w.State != "open" && w.State != "sealed" {
484
+ continue
485
+ }
486
+ var pending []string
487
+ for role, r := range w.Roles {
488
+ if !r.Returned {
489
+ pending = append(pending, role)
490
+ }
491
+ }
492
+ sort.Strings(pending)
493
+ out = append(out, InflightWave{Wave: name, Phase: w.Phase, State: w.State, Pending: pending, Slices: w.Slices})
494
+ }
495
+ sort.Slice(out, func(i, j int) bool { return out[i].Wave < out[j].Wave })
496
+ return out
497
+ }
498
+
460
499
  func waveState(w *dispatchWave) string {
461
500
  if w == nil {
462
501
  return "absent"
@@ -7,6 +7,7 @@ import (
7
7
  "io"
8
8
  "os"
9
9
  "path/filepath"
10
+ "slices"
10
11
  "sort"
11
12
  "strconv"
12
13
  "strings"
@@ -23,7 +24,7 @@ const MetricsFile = "metrics.jsonl"
23
24
  const metricsUsage = `usage: devrites-engine metrics <record|summary> ...
24
25
 
25
26
  metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]
26
- metrics summary [slug]
27
+ metrics summary [slug] [--by-slice]
27
28
  `
28
29
 
29
30
  type metricEvent struct {
@@ -150,6 +151,8 @@ func recordMetric(root, slug, phase, event, role string, bytes int64) {
150
151
  }
151
152
 
152
153
  func metricsSummary(root string, args []string, stdout, stderr io.Writer) int {
154
+ bySlice := slices.Contains(args, "--by-slice")
155
+ args = slices.DeleteFunc(slices.Clone(args), func(a string) bool { return a == "--by-slice" })
153
156
  slug, code, err := ActiveSlug(root, args)
154
157
  if err != nil {
155
158
  fmt.Fprintf(stderr, "metrics: %v\n", err)
@@ -163,6 +166,9 @@ func metricsSummary(root string, args []string, stdout, stderr io.Writer) int {
163
166
  fmt.Fprintf(stderr, "metrics: %v\n", err)
164
167
  return 2
165
168
  }
169
+ if bySlice {
170
+ return sliceCostSummary(root, slug, stdout, stderr)
171
+ }
166
172
  f, err := os.Open(filepath.Join(dir, MetricsFile)) // #nosec G304 -- dir is a validated workspace and filename is fixed
167
173
  if err != nil {
168
174
  fmt.Fprintf(stdout, "metrics: no events recorded for %s\n", slug)
@@ -258,3 +264,70 @@ func metricsSummary(root string, args []string, stdout, stderr io.Writer) int {
258
264
  fmt.Fprintln(stdout)
259
265
  return 0
260
266
  }
267
+
268
+ // sliceCostSummary reports, per slice named on dispatch waves, the build wright
269
+ // rounds, slice-bound Vet rechecks, and agent-minutes (role start to return; a
270
+ // wave naming several slices splits its minutes evenly). Abandoned waves are
271
+ // excluded, and waves recorded before returns were timestamped count rounds only.
272
+ func sliceCostSummary(root, slug string, stdout, stderr io.Writer) int {
273
+ f, _, err := loadDispatch(root, slug)
274
+ if err != nil {
275
+ fmt.Fprintf(stderr, "metrics: %v\n", err)
276
+ return 2
277
+ }
278
+ type cost struct {
279
+ wright, vet int
280
+ minutes float64
281
+ timed bool
282
+ }
283
+ per := map[string]*cost{}
284
+ for _, w := range f.Waves {
285
+ kind := roundKind(w)
286
+ if kind == "" || w.State == "abandoned" {
287
+ continue
288
+ }
289
+ minutes, timed := 0.0, false
290
+ for _, r := range w.Roles {
291
+ start, startErr := time.Parse(time.RFC3339, r.StartedAt)
292
+ end, endErr := time.Parse(time.RFC3339, r.ReturnedAt)
293
+ if startErr == nil && endErr == nil && !end.Before(start) {
294
+ minutes += end.Sub(start).Minutes()
295
+ timed = true
296
+ }
297
+ }
298
+ for _, id := range w.Slices {
299
+ c := per[id]
300
+ if c == nil {
301
+ c = &cost{}
302
+ per[id] = c
303
+ }
304
+ if kind == "wright" {
305
+ c.wright++
306
+ } else {
307
+ c.vet++
308
+ }
309
+ if timed {
310
+ c.minutes += minutes / float64(len(w.Slices))
311
+ c.timed = true
312
+ }
313
+ }
314
+ }
315
+ if len(per) == 0 {
316
+ fmt.Fprintf(stdout, "metrics: no slice-tagged dispatch waves for %s\n", slug)
317
+ return 0
318
+ }
319
+ ids := make([]string, 0, len(per))
320
+ for id := range per {
321
+ ids = append(ids, id)
322
+ }
323
+ sort.Strings(ids)
324
+ for _, id := range ids {
325
+ c := per[id]
326
+ minutes := "n/a"
327
+ if c.timed {
328
+ minutes = strconv.FormatFloat(c.minutes, 'f', 0, 64)
329
+ }
330
+ fmt.Fprintf(stdout, "metrics: %s wright_rounds=%d vet_rechecks=%d agent_minutes=%s\n", id, c.wright, c.vet, minutes)
331
+ }
332
+ return 0
333
+ }
@@ -394,3 +394,35 @@ func TestObserveArtifactBudgetsOversizedArtifactIsOverWithoutFullRead(t *testing
394
394
  t.Fatalf("allocated %d bytes for a %d byte artifact; the read must be bounded", allocated, size)
395
395
  }
396
396
  }
397
+
398
+ func TestUnreturnedWavesWarnAndSurfaceInOrient(t *testing.T) {
399
+ root, _ := writeBudgetWorkspace(t, map[string]string{
400
+ "state.md": "| phase | build |\n| schema | 4 |\n",
401
+ "tasks.md": budgetTasks,
402
+ })
403
+ open := func(wave string) string {
404
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
405
+ args := []string{"feature", "open", "--phase", "build", "--wave", wave, "--role", "slice-wright", "--slice", "SLICE-001"}
406
+ if code := RunDispatch(root, args, stdout, stderr); code != 0 {
407
+ t.Fatalf("open %s: code=%d\n%s%s", wave, code, stdout, stderr)
408
+ }
409
+ return stdout.String()
410
+ }
411
+ if out := open("r1"); strings.Contains(out, "WARNING") {
412
+ t.Fatalf("first wave must not warn:\n%s", out)
413
+ }
414
+ if out := open("r2"); !strings.Contains(out, "WARNING: SLICE-001 earlier wave(s) r1 never returned") {
415
+ t.Fatalf("unreturned earlier wave not warned:\n%s", out)
416
+ }
417
+ summary, err := ObserveSummaryFor(root, "feature")
418
+ if err != nil {
419
+ t.Fatal(err)
420
+ }
421
+ if len(summary.InflightWaves) != 2 {
422
+ t.Fatalf("inflight waves=%+v", summary.InflightWaves)
423
+ }
424
+ first := summary.InflightWaves[0]
425
+ if first.Wave != "r1" || first.State != "open" || len(first.Pending) != 1 || first.Pending[0] != "slice-wright" || first.Slices[0] != "SLICE-001" {
426
+ t.Fatalf("inflight wave=%+v", first)
427
+ }
428
+ }
@@ -51,6 +51,7 @@ type ObserveSummary struct {
51
51
  ArtifactBudgets []ArtifactBudget `json:"artifact_budgets,omitempty"`
52
52
  BulkFiles []BulkFile `json:"bulk_files,omitempty"`
53
53
  Sequence *ObserveSequenceCursor `json:"sequence,omitempty"`
54
+ InflightWaves []InflightWave `json:"inflight_waves,omitempty"`
54
55
  }
55
56
 
56
57
  // ObserveSummaryFor builds a summary for one feature slug.
@@ -91,6 +92,13 @@ func ObserveSummaryFor(root, slug string) (ObserveSummary, error) {
91
92
  OK: len(graph.Problems) == 0,
92
93
  }
93
94
  }
95
+ // Open or sealed waves are agents whose results the root never recorded;
96
+ // a resume reconciles them before starting new work.
97
+ if waves, _, err := loadDispatch(root, slug); err == nil {
98
+ summary.InflightWaves = inflightWaves(waves)
99
+ } else {
100
+ summary.Unreadable = append(summary.Unreadable, "dispatch.json")
101
+ }
94
102
  if report.SequenceParent != "" || report.SequencePosition != "" || report.SequenceWorkspacesRemaining != "" || report.SequenceRole != "" {
95
103
  summary.Sequence = &ObserveSequenceCursor{
96
104
  Parent: report.SequenceParent,
package/engine/main.go CHANGED
@@ -51,7 +51,7 @@ Usage:
51
51
  devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--slice <id>] [--trigger a,b]
52
52
  Emit one deduplicated read-set bundle for a phase, skill, or dispatch role
53
53
  devrites-engine metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]
54
- devrites-engine metrics summary [slug] Summarize the per-feature metrics.jsonl event ledger
54
+ devrites-engine metrics summary [slug] [--by-slice] Summarize the metrics ledger, or per-slice rounds and agent-minutes
55
55
  devrites-engine dispatch <slug> <sub> Launch-wave barrier for parallel dispatch: open/start/seal/return/status/abandon (build wright waves: --slice, round cap)
56
56
  devrites-engine claim <add|release|list|check> Advisory session-scoped file claims for same-tree concurrent sessions (.devrites/claims.jsonl)
57
57
  devrites-engine note <subcommand> <slug> Anchored workspace notes bound to verbatim code quotes; drift regrades them (notes.md)
@@ -120,7 +120,10 @@ unclear boundary is underspecified: return `Escalation` without proceeding.
120
120
  new fingerprint. At the limit return the gate and repro. `Escalation` is only for product/irreversible choices
121
121
  or human-only access; technical failure is a blocker.
122
122
  Before handoff, inspect your cumulative slice diff against the contract and
123
- scenario matrix, including correction-affected adjacent variants. Fix supported
123
+ scenario matrix, including correction-affected adjacent variants, then against
124
+ the reviewers' own checks: [code review § What to check](../skills/devrites-lib/reference/standards/code-review.md#what-to-check-tests-first)
125
+ and test analysis (each acceptance criterion has a test that fails without the
126
+ change; no assertion-free, tautological, or mock-asserting test). Fix supported
124
127
  in-scope gaps together, rerun affected proof, and report remaining gaps. Self-check
125
128
  never replaces independent review; green tests do not prove all possible cases.
126
129
  5. **TRANSFER when isolated.** After green writer-safe proof, verify the diff again,
@@ -9,7 +9,9 @@ contract for the sentinel schema, defaults, gate ceiling, and mutable-counter
9
9
  ownership. This file owns only Build's dispatch, charging, and red-path behavior.
10
10
 
11
11
  Rules: green before `built`; hard `max_slices` cap; gates before the action they
12
- control; irreversible work (destructive/auth/public API) always pauses.
12
+ control; irreversible work (destructive/auth/public API) always pauses; a tick
13
+ never ends with an open or sealed wave — wait for its returns or
14
+ `dispatch abandon --reason`.
13
15
 
14
16
  ## Iteration cap
15
17
 
@@ -9,7 +9,7 @@ Evidence: acceptance met; tests <cmd> → pass; browser <summary|n/a>; drift <no
9
9
  Open: <none|blockers|questions>
10
10
  Next: /rite-build
11
11
  Record: .devrites/work/<slug>/evidence.md
12
- ↻ Hygiene: /clear between slices; /rite-handoff if away > a few hours
12
+ ↻ Hygiene: /clear between slices (none in `orient` `inflight_waves`); /rite-handoff if away > a few hours
13
13
  ```
14
14
 
15
15
  **All built:**
@@ -14,6 +14,8 @@ Spec Drift Guard still applies.
14
14
  `Implementation readiness: READY`, current `Readiness inputs SHA-256`, exact
15
15
  required accounts, ID/meaning traceability and green proof preflight.
16
16
  Run `devrites-engine check readiness <slug>`; any miss/nonzero blocks.
17
+ Non-empty `orient` `inflight_waves` is reconciled first: `dispatch return` each
18
+ role whose account is in hand, else `dispatch abandon --reason` and re-dispatch.
17
19
  2. **Select.** Pick the lowest pending slice whose `depends_on` are all built, using
18
20
  `orient` `task_graph.slices` plus the `state.md` built list — never by scanning
19
21
  `tasks.md`; then `observe slice` it. Restate its goal, ACs, exclusions and exact paths. Include
@@ -120,7 +120,10 @@ unclear boundary is underspecified: return `Escalation` without proceeding.
120
120
  new fingerprint. At the limit return the gate and repro. `Escalation` is only for product/irreversible choices
121
121
  or human-only access; technical failure is a blocker.
122
122
  Before handoff, inspect your cumulative slice diff against the contract and
123
- scenario matrix, including correction-affected adjacent variants. Fix supported
123
+ scenario matrix, including correction-affected adjacent variants, then against
124
+ the reviewers' own checks: [code review § What to check](../skills/devrites-lib/reference/standards/code-review.md#what-to-check-tests-first)
125
+ and test analysis (each acceptance criterion has a test that fails without the
126
+ change; no assertion-free, tautological, or mock-asserting test). Fix supported
124
127
  in-scope gaps together, rerun affected proof, and report remaining gaps. Self-check
125
128
  never replaces independent review; green tests do not prove all possible cases.
126
129
  5. **TRANSFER when isolated.** After green writer-safe proof, verify the diff again,
@@ -9,7 +9,9 @@ contract for the sentinel schema, defaults, gate ceiling, and mutable-counter
9
9
  ownership. This file owns only Build's dispatch, charging, and red-path behavior.
10
10
 
11
11
  Rules: green before `built`; hard `max_slices` cap; gates before the action they
12
- control; irreversible work (destructive/auth/public API) always pauses.
12
+ control; irreversible work (destructive/auth/public API) always pauses; a tick
13
+ never ends with an open or sealed wave — wait for its returns or
14
+ `dispatch abandon --reason`.
13
15
 
14
16
  ## Iteration cap
15
17
 
@@ -9,7 +9,7 @@ Evidence: acceptance met; tests <cmd> → pass; browser <summary|n/a>; drift <no
9
9
  Open: <none|blockers|questions>
10
10
  Next: /rite-build
11
11
  Record: .devrites/work/<slug>/evidence.md
12
- ↻ Hygiene: /clear between slices; /rite-handoff if away > a few hours
12
+ ↻ Hygiene: /clear between slices (none in `orient` `inflight_waves`); /rite-handoff if away > a few hours
13
13
  ```
14
14
 
15
15
  **All built:**
@@ -14,6 +14,8 @@ Spec Drift Guard still applies.
14
14
  `Implementation readiness: READY`, current `Readiness inputs SHA-256`, exact
15
15
  required accounts, ID/meaning traceability and green proof preflight.
16
16
  Run `devrites-engine check readiness <slug>`; any miss/nonzero blocks.
17
+ Non-empty `orient` `inflight_waves` is reconciled first: `dispatch return` each
18
+ role whose account is in hand, else `dispatch abandon --reason` and re-dispatch.
17
19
  2. **Select.** Pick the lowest pending slice whose `depends_on` are all built, using
18
20
  `orient` `task_graph.slices` plus the `state.md` built list — never by scanning
19
21
  `tasks.md`; then `observe slice` it. Restate its goal, ACs, exclusions and exact paths. Include
@@ -118,7 +118,10 @@ unclear boundary is underspecified: return `Escalation` without proceeding.
118
118
  new fingerprint. At the limit return the gate and repro. `Escalation` is only for product/irreversible choices
119
119
  or human-only access; technical failure is a blocker.
120
120
  Before handoff, inspect your cumulative slice diff against the contract and
121
- scenario matrix, including correction-affected adjacent variants. Fix supported
121
+ scenario matrix, including correction-affected adjacent variants, then against
122
+ the reviewers' own checks: [code review § What to check](../skills/devrites-lib/reference/standards/code-review.md#what-to-check-tests-first)
123
+ and test analysis (each acceptance criterion has a test that fails without the
124
+ change; no assertion-free, tautological, or mock-asserting test). Fix supported
122
125
  in-scope gaps together, rerun affected proof, and report remaining gaps. Self-check
123
126
  never replaces independent review; green tests do not prove all possible cases.
124
127
  5. **TRANSFER when isolated.** After green writer-safe proof, verify the diff again,
@@ -9,7 +9,9 @@ contract for the sentinel schema, defaults, gate ceiling, and mutable-counter
9
9
  ownership. This file owns only Build's dispatch, charging, and red-path behavior.
10
10
 
11
11
  Rules: green before `built`; hard `max_slices` cap; gates before the action they
12
- control; irreversible work (destructive/auth/public API) always pauses.
12
+ control; irreversible work (destructive/auth/public API) always pauses; a tick
13
+ never ends with an open or sealed wave — wait for its returns or
14
+ `dispatch abandon --reason`.
13
15
 
14
16
  ## Iteration cap
15
17
 
@@ -9,7 +9,7 @@ Evidence: acceptance met; tests <cmd> → pass; browser <summary|n/a>; drift <no
9
9
  Open: <none|blockers|questions>
10
10
  Next: $rite-build
11
11
  Record: .devrites/work/<slug>/evidence.md
12
- ↻ Hygiene: /clear between slices; $rite-handoff if away > a few hours
12
+ ↻ Hygiene: /clear between slices (none in `orient` `inflight_waves`); $rite-handoff if away > a few hours
13
13
  ```
14
14
 
15
15
  **All built:**
@@ -14,6 +14,8 @@ Spec Drift Guard still applies.
14
14
  `Implementation readiness: READY`, current `Readiness inputs SHA-256`, exact
15
15
  required accounts, ID/meaning traceability and green proof preflight.
16
16
  Run `devrites-engine check readiness <slug>`; any miss/nonzero blocks.
17
+ Non-empty `orient` `inflight_waves` is reconciled first: `dispatch return` each
18
+ role whose account is in hand, else `dispatch abandon --reason` and re-dispatch.
17
19
  2. **Select.** Pick the lowest pending slice whose `depends_on` are all built, using
18
20
  `orient` `task_graph.slices` plus the `state.md` built list — never by scanning
19
21
  `tasks.md`; then `observe slice` it. Restate its goal, ACs, exclusions and exact paths. Include
@@ -125,7 +125,10 @@ unclear boundary is underspecified: return `Escalation` without proceeding.
125
125
  new fingerprint. At the limit return the gate and repro. `Escalation` is only for product/irreversible choices
126
126
  or human-only access; technical failure is a blocker.
127
127
  Before handoff, inspect your cumulative slice diff against the contract and
128
- scenario matrix, including correction-affected adjacent variants. Fix supported
128
+ scenario matrix, including correction-affected adjacent variants, then against
129
+ the reviewers' own checks: [code review § What to check](../skills/devrites-lib/reference/standards/code-review.md#what-to-check-tests-first)
130
+ and test analysis (each acceptance criterion has a test that fails without the
131
+ change; no assertion-free, tautological, or mock-asserting test). Fix supported
129
132
  in-scope gaps together, rerun affected proof, and report remaining gaps. Self-check
130
133
  never replaces independent review; green tests do not prove all possible cases.
131
134
  5. **TRANSFER when isolated.** After green writer-safe proof, verify the diff again,
@@ -9,7 +9,9 @@ contract for the sentinel schema, defaults, gate ceiling, and mutable-counter
9
9
  ownership. This file owns only Build's dispatch, charging, and red-path behavior.
10
10
 
11
11
  Rules: green before `built`; hard `max_slices` cap; gates before the action they
12
- control; irreversible work (destructive/auth/public API) always pauses.
12
+ control; irreversible work (destructive/auth/public API) always pauses; a tick
13
+ never ends with an open or sealed wave — wait for its returns or
14
+ `dispatch abandon --reason`.
13
15
 
14
16
  ## Iteration cap
15
17
 
@@ -9,7 +9,7 @@ Evidence: acceptance met; tests <cmd> → pass; browser <summary|n/a>; drift <no
9
9
  Open: <none|blockers|questions>
10
10
  Next: /rite-build
11
11
  Record: .devrites/work/<slug>/evidence.md
12
- ↻ Hygiene: /clear between slices; /rite-handoff if away > a few hours
12
+ ↻ Hygiene: /clear between slices (none in `orient` `inflight_waves`); /rite-handoff if away > a few hours
13
13
  ```
14
14
 
15
15
  **All built:**
@@ -14,6 +14,8 @@ Spec Drift Guard still applies.
14
14
  `Implementation readiness: READY`, current `Readiness inputs SHA-256`, exact
15
15
  required accounts, ID/meaning traceability and green proof preflight.
16
16
  Run `devrites-engine check readiness <slug>`; any miss/nonzero blocks.
17
+ Non-empty `orient` `inflight_waves` is reconciled first: `dispatch return` each
18
+ role whose account is in hand, else `dispatch abandon --reason` and re-dispatch.
17
19
  2. **Select.** Pick the lowest pending slice whose `depends_on` are all built, using
18
20
  `orient` `task_graph.slices` plus the `state.md` built list — never by scanning
19
21
  `tasks.md`; then `observe slice` it. Restate its goal, ACs, exclusions and exact paths. Include
@@ -118,7 +118,10 @@ unclear boundary is underspecified: return `Escalation` without proceeding.
118
118
  new fingerprint. At the limit return the gate and repro. `Escalation` is only for product/irreversible choices
119
119
  or human-only access; technical failure is a blocker.
120
120
  Before handoff, inspect your cumulative slice diff against the contract and
121
- scenario matrix, including correction-affected adjacent variants. Fix supported
121
+ scenario matrix, including correction-affected adjacent variants, then against
122
+ the reviewers' own checks: [code review § What to check](../skills/devrites-lib/reference/standards/code-review.md#what-to-check-tests-first)
123
+ and test analysis (each acceptance criterion has a test that fails without the
124
+ change; no assertion-free, tautological, or mock-asserting test). Fix supported
122
125
  in-scope gaps together, rerun affected proof, and report remaining gaps. Self-check
123
126
  never replaces independent review; green tests do not prove all possible cases.
124
127
  5. **TRANSFER when isolated.** After green writer-safe proof, verify the diff again,
@@ -9,7 +9,9 @@ contract for the sentinel schema, defaults, gate ceiling, and mutable-counter
9
9
  ownership. This file owns only Build's dispatch, charging, and red-path behavior.
10
10
 
11
11
  Rules: green before `built`; hard `max_slices` cap; gates before the action they
12
- control; irreversible work (destructive/auth/public API) always pauses.
12
+ control; irreversible work (destructive/auth/public API) always pauses; a tick
13
+ never ends with an open or sealed wave — wait for its returns or
14
+ `dispatch abandon --reason`.
13
15
 
14
16
  ## Iteration cap
15
17
 
@@ -9,7 +9,7 @@ Evidence: acceptance met; tests <cmd> → pass; browser <summary|n/a>; drift <no
9
9
  Open: <none|blockers|questions>
10
10
  Next: /rite-build
11
11
  Record: .devrites/work/<slug>/evidence.md
12
- ↻ Hygiene: /clear between slices; /rite-handoff if away > a few hours
12
+ ↻ Hygiene: /clear between slices (none in `orient` `inflight_waves`); /rite-handoff if away > a few hours
13
13
  ```
14
14
 
15
15
  **All built:**
@@ -14,6 +14,8 @@ Spec Drift Guard still applies.
14
14
  `Implementation readiness: READY`, current `Readiness inputs SHA-256`, exact
15
15
  required accounts, ID/meaning traceability and green proof preflight.
16
16
  Run `devrites-engine check readiness <slug>`; any miss/nonzero blocks.
17
+ Non-empty `orient` `inflight_waves` is reconciled first: `dispatch return` each
18
+ role whose account is in hand, else `dispatch abandon --reason` and re-dispatch.
17
19
  2. **Select.** Pick the lowest pending slice whose `depends_on` are all built, using
18
20
  `orient` `task_graph.slices` plus the `state.md` built list — never by scanning
19
21
  `tasks.md`; then `observe slice` it. Restate its goal, ACs, exclusions and exact paths. Include
@@ -119,7 +119,10 @@ unclear boundary is underspecified: return `Escalation` without proceeding.
119
119
  new fingerprint. At the limit return the gate and repro. `Escalation` is only for product/irreversible choices
120
120
  or human-only access; technical failure is a blocker.
121
121
  Before handoff, inspect your cumulative slice diff against the contract and
122
- scenario matrix, including correction-affected adjacent variants. Fix supported
122
+ scenario matrix, including correction-affected adjacent variants, then against
123
+ the reviewers' own checks: [code review § What to check](../skills/devrites-lib/reference/standards/code-review.md#what-to-check-tests-first)
124
+ and test analysis (each acceptance criterion has a test that fails without the
125
+ change; no assertion-free, tautological, or mock-asserting test). Fix supported
123
126
  in-scope gaps together, rerun affected proof, and report remaining gaps. Self-check
124
127
  never replaces independent review; green tests do not prove all possible cases.
125
128
  5. **TRANSFER when isolated.** After green writer-safe proof, verify the diff again,
@@ -9,7 +9,9 @@ contract for the sentinel schema, defaults, gate ceiling, and mutable-counter
9
9
  ownership. This file owns only Build's dispatch, charging, and red-path behavior.
10
10
 
11
11
  Rules: green before `built`; hard `max_slices` cap; gates before the action they
12
- control; irreversible work (destructive/auth/public API) always pauses.
12
+ control; irreversible work (destructive/auth/public API) always pauses; a tick
13
+ never ends with an open or sealed wave — wait for its returns or
14
+ `dispatch abandon --reason`.
13
15
 
14
16
  ## Iteration cap
15
17
 
@@ -9,7 +9,7 @@ Evidence: acceptance met; tests <cmd> → pass; browser <summary|n/a>; drift <no
9
9
  Open: <none|blockers|questions>
10
10
  Next: /rite-build
11
11
  Record: .devrites/work/<slug>/evidence.md
12
- ↻ Hygiene: /clear between slices; /rite-handoff if away > a few hours
12
+ ↻ Hygiene: /clear between slices (none in `orient` `inflight_waves`); /rite-handoff if away > a few hours
13
13
  ```
14
14
 
15
15
  **All built:**
@@ -14,6 +14,8 @@ Spec Drift Guard still applies.
14
14
  `Implementation readiness: READY`, current `Readiness inputs SHA-256`, exact
15
15
  required accounts, ID/meaning traceability and green proof preflight.
16
16
  Run `devrites-engine check readiness <slug>`; any miss/nonzero blocks.
17
+ Non-empty `orient` `inflight_waves` is reconciled first: `dispatch return` each
18
+ role whose account is in hand, else `dispatch abandon --reason` and re-dispatch.
17
19
  2. **Select.** Pick the lowest pending slice whose `depends_on` are all built, using
18
20
  `orient` `task_graph.slices` plus the `state.md` built list — never by scanning
19
21
  `tasks.md`; then `observe slice` it. Restate its goal, ACs, exclusions and exact paths. Include
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "6.0.3",
3
+ "version": "6.0.4",
4
4
  "description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code, Codex, omp, pi, and Devin CLI",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://github.com/ViktorsBaikers/DevRites#readme",