devrites 3.2.7 → 3.2.9

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 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.2.9](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.8...v3.2.9) (2026-07-26)
6
+
7
+ ### Fixed
8
+
9
+ * **devrites:** unblock retained reconcile checks ([c1093b0](https://github.com/ViktorsBaikers/DevRites/commit/c1093b00c3e32ba3cc22e0b9a2da548618ead621))
10
+
11
+ ## [3.2.8](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.7...v3.2.8) (2026-07-26)
12
+
13
+ ### Fixed
14
+
15
+ * **devrites:** prevent reconcile self-invalidation ([161ed18](https://github.com/ViktorsBaikers/DevRites/commit/161ed187e600dfc92623b22db5cd7f0824408d1a))
16
+
5
17
  ## [3.2.7](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.6...v3.2.7) (2026-07-26)
6
18
 
7
19
  ### Fixed
package/README.md CHANGED
@@ -25,7 +25,7 @@ final commit, push, and tag, and it requires a typed `GO` confirmation.
25
25
  Unattended runs may create local WIP checkpoint commits along the way, but only
26
26
  Ship collapses and pushes them.
27
27
 
28
- **Status:** [`v3.2.7`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.7): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
28
+ **Status:** [`v3.2.9`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.9): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -69,6 +69,24 @@ Each dispatch follows the retained-baseline sequence:
69
69
  5. After proof and decision checks pass, `reconcile close` retires the private
70
70
  window; only then does the root write canonical records.
71
71
 
72
+ An ordinary `reconcile check` may revalidate an existing retained window
73
+ without a fresh current-turn wright receipt; it does not retire or widen that
74
+ window. `reconcile close` still requires the confirmed, awaited wright result
75
+ bound to the active snapshot. While the window is armed, the root guard permits
76
+ inspectable writes only under `.devrites` or at an absolute scratch path outside
77
+ the project; source-tree writes, relative escapes, and opaque execution remain
78
+ blocked.
79
+
80
+ The canonical-state fingerprint excludes root-owned operational records:
81
+ `timeline.jsonl`, feature `events.jsonl`, a valid `recovery-attempts.jsonl`,
82
+ the redwatch sentinel, automatic handoff snapshots, and hidden hook observation
83
+ logs. Those records cannot grant source authority or invalidate a writer result
84
+ merely because the engine or a hook wrote them. Unknown or canonical
85
+ `.devrites` paths remain fail-closed, and recovery ledgers must still parse
86
+ before reconciliation can pass. Gate-authoritative operational state such as
87
+ recovery attempts and `.red` remains enforced by its owning gate. Checks also
88
+ normalize these paths out of retained snapshots created by older engines.
89
+
72
90
  On retry, the root may add only an accepted still-in-slice path, then runs
73
91
  `reconcile snapshot` again. This refreshes the dispatch boundary while
74
92
  preserving the original slice baseline. The same objective root cause has a
@@ -33,7 +33,7 @@ var (
33
33
  devritesAgentPathRe = regexp.MustCompile(`\.codex/agents/(devrites-[a-z0-9-]+)\.toml`)
34
34
  devritesAgentNameRe = regexp.MustCompile(`^devrites-[a-z0-9-]+$`)
35
35
  skillInvocationRe = regexp.MustCompile(`(?i)(?:^|[^a-z0-9_./-])(?:\$|/)([a-z][a-z0-9-]*)\b`)
36
- reconcileTerminalRe = regexp.MustCompile(`(?m)(?:^|[;&|]\s*)(?:rtk\s+)?(?:[A-Za-z0-9_./-]+/)?devrites-engine\s+reconcile\s+(?:check|close)\b`)
36
+ reconcileCloseRe = regexp.MustCompile(`(?m)(?:^|[;&|]\s*)(?:rtk\s+)?(?:[A-Za-z0-9_./-]+/)?devrites-engine\s+reconcile\s+close\b`)
37
37
  )
38
38
 
39
39
  type agentDispatchHookInput struct {
@@ -1163,7 +1163,7 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
1163
1163
  return exitOK
1164
1164
  }
1165
1165
 
1166
- if isShellTool(in.ToolName) && reconcileTerminalRe.MatchString(in.ToolInput.Command) {
1166
+ if isShellTool(in.ToolName) && reconcileCloseRe.MatchString(in.ToolInput.Command) {
1167
1167
  windowID := currentReconcileWindowID()
1168
1168
  if windowID == "" {
1169
1169
  return exitOK
@@ -1177,7 +1177,7 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
1177
1177
  }
1178
1178
  attempts = append(attempts, durable...)
1179
1179
  if !roleSatisfied("devrites-slice-wright", windowID, attempts) {
1180
- return preToolDeny(h, "DevRites reconcile check/close requires a confirmed, awaited devrites-slice-wright result bound to the active reconcile snapshot.", stdout, stderr)
1180
+ return preToolDeny(h, "DevRites reconcile close requires a confirmed, awaited devrites-slice-wright result bound to the active reconcile snapshot.", stdout, stderr)
1181
1181
  }
1182
1182
  for _, attempt := range durable {
1183
1183
  if attempt.Role != "devrites-slice-wright" || attempt.WindowID != windowID ||
@@ -1024,8 +1024,9 @@ func guardRootBuildWindow(h harness.Harness, data []byte, in harness.GuardInput,
1024
1024
  }
1025
1025
  targets, suspicious := rootMutationTargets(data, in)
1026
1026
  for _, target := range targets {
1027
+ absoluteTarget := filepath.IsAbs(target)
1027
1028
  abs := target
1028
- if !filepath.IsAbs(abs) {
1029
+ if !absoluteTarget {
1029
1030
  abs = filepath.Join(projectDir, abs)
1030
1031
  }
1031
1032
  abs = filepath.Clean(abs)
@@ -1033,7 +1034,9 @@ func guardRootBuildWindow(h harness.Harness, data []byte, in harness.GuardInput,
1033
1034
  if err != nil {
1034
1035
  return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
1035
1036
  }
1036
- if !underDevrites(resolved, root) {
1037
+ insideProject := pathWithin(resolved, projectDir)
1038
+ if (insideProject && !underDevrites(resolved, root)) ||
1039
+ (!insideProject && !absoluteTarget) {
1037
1040
  return denyWright(h, stdout, stderr, a1DenyReason, drvreason.HookA1Denied), true
1038
1041
  }
1039
1042
  }
@@ -215,6 +215,7 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
215
215
  fmt.Fprintf(stderr, "reconcile: invalid .devrites snapshot: %v\n", err)
216
216
  return 6
217
217
  }
218
+ beforeDevrites = withoutRootOwnedOperationalState(beforeDevrites)
218
219
  afterDevrites, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked)
219
220
  if err != nil {
220
221
  fmt.Fprintf(stderr, "reconcile: cannot compare .devrites: %v\n", err)
@@ -465,6 +466,13 @@ func captureDevritesState(root string, ignoredPaths ...string) ([]devritesStateE
465
466
  }
466
467
  return nil
467
468
  }
469
+ operational, err := reconcileRootOwnedOperationalFile(filename, rel, entry)
470
+ if err != nil {
471
+ return err
472
+ }
473
+ if operational {
474
+ return nil
475
+ }
468
476
 
469
477
  fingerprint, err := devritesFingerprint(filename, entry)
470
478
  if err != nil {
@@ -483,6 +491,61 @@ func captureDevritesState(root string, ignoredPaths ...string) ([]devritesStateE
483
491
  return state, nil
484
492
  }
485
493
 
494
+ // reconcileRootOwnedOperationalFile excludes engine/root-owned operational
495
+ // records from the writer delta. Their owning hooks/gates validate them where
496
+ // needed; root activity must not be attributed to the slice writer.
497
+ func reconcileRootOwnedOperationalFile(filename, rel string, entry os.DirEntry) (bool, error) {
498
+ if !entry.Type().IsRegular() || !reconcileRootOwnedOperationalPath(rel) {
499
+ return false, nil
500
+ }
501
+ rel = filepath.ToSlash(filepath.Clean(rel))
502
+ parts := strings.Split(rel, "/")
503
+ if len(parts) == 3 && parts[2] == recoveryAttemptsFile {
504
+ if _, err := readRecoveryAttempts(filename); err != nil {
505
+ return false, fmt.Errorf("validate root-owned recovery ledger %s: %w", rel, err)
506
+ }
507
+ }
508
+ return true, nil
509
+ }
510
+
511
+ func reconcileRootOwnedOperationalPath(rel string) bool {
512
+ rel = filepath.ToSlash(filepath.Clean(rel))
513
+ if rel == "timeline.jsonl" {
514
+ return true
515
+ }
516
+ parts := strings.Split(rel, "/")
517
+ if len(parts) != 3 ||
518
+ (parts[0] != "work" && parts[0] != "features") ||
519
+ !slugToken.MatchString(parts[1]) {
520
+ return false
521
+ }
522
+ switch parts[2] {
523
+ case "events.jsonl",
524
+ recoveryAttemptsFile,
525
+ ".red",
526
+ "handoff.md",
527
+ ".a1-guard.log",
528
+ ".reviewer-ro.log",
529
+ ".stop-gate.log",
530
+ ".wright-scope.log":
531
+ return true
532
+ default:
533
+ return false
534
+ }
535
+ }
536
+
537
+ func withoutRootOwnedOperationalState(state []devritesStateEntry) []devritesStateEntry {
538
+ filtered := state[:0]
539
+ for _, item := range state {
540
+ if strings.HasPrefix(item.Fingerprint, "file:") &&
541
+ reconcileRootOwnedOperationalPath(item.Path) {
542
+ continue
543
+ }
544
+ filtered = append(filtered, item)
545
+ }
546
+ return filtered
547
+ }
548
+
486
549
  func devritesFingerprint(filename string, entry os.DirEntry) (string, error) {
487
550
  info, err := entry.Info()
488
551
  if err != nil {
@@ -361,6 +361,158 @@ func TestReconcileRejectsDevritesMutation(t *testing.T) {
361
361
  }
362
362
  }
363
363
 
364
+ func TestReconcileAllowsRootOwnedOperationalFiles(t *testing.T) {
365
+ newGitRepo(t)
366
+ root := workspace(t, "feat")
367
+ dir := featureDir(root, "feat")
368
+ writeWrightAllowlist(t, root, "feat", "seed.go")
369
+ if code := Stuck(root, []string{"log", "feat", "dispatch", "SLICE-010"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
370
+ t.Fatalf("stuck log = %d, want 0", code)
371
+ }
372
+ if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
373
+ t.Fatalf("snapshot = %d, want 0\n%s", code, out)
374
+ }
375
+
376
+ writeFile(t, filepath.Join(root, "timeline.jsonl"), "telemetry\n")
377
+ writeFile(t, filepath.Join(dir, "events.jsonl"), "telemetry\n")
378
+ writeFile(t, filepath.Join(dir, ".a1-guard.log"), "WOULD-BLOCK\n")
379
+ writeFile(t, filepath.Join(dir, ".reviewer-ro.log"), "WOULD-BLOCK\n")
380
+ writeFile(t, filepath.Join(dir, ".stop-gate.log"), "WOULD-BLOCK\n")
381
+ writeFile(t, filepath.Join(dir, ".wright-scope.log"), "WOULD-BLOCK\n")
382
+ writeFile(t, filepath.Join(dir, ".red"), "npm test\n")
383
+ writeFile(t, filepath.Join(dir, "handoff.md"), "## Handoff snapshot\n")
384
+ if code := RecoveryAttempts(
385
+ root,
386
+ []string{"record", "--class", "proof_tool_defect", "reconcile telemetry", "changed logs", "feat"},
387
+ &bytes.Buffer{},
388
+ &bytes.Buffer{},
389
+ ); code != 0 {
390
+ t.Fatalf("recovery record = %d, want 0", code)
391
+ }
392
+
393
+ if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
394
+ t.Fatalf("check = %d, want 0\n%s", code, out)
395
+ }
396
+ if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
397
+ t.Fatalf("refresh = %d, want 0\n%s", code, out)
398
+ }
399
+
400
+ writeFile(t, filepath.Join(dir, ".unknown.log"), "not engine-owned\n")
401
+ code, out := runReconcile(t, root, "check", "feat")
402
+ if code != 5 {
403
+ t.Fatalf("check after canonical mutation = %d, want 5\n%s", code, out)
404
+ }
405
+ if !strings.Contains(out, ".devrites/work/feat/.unknown.log") {
406
+ t.Fatalf("canonical mutation missing from output:\n%s", out)
407
+ }
408
+ }
409
+
410
+ func TestReconcileAcceptsLegacyOperationalFingerprints(t *testing.T) {
411
+ newGitRepo(t)
412
+ root := workspace(t, "feat")
413
+ dir := featureDir(root, "feat")
414
+ writeWrightAllowlist(t, root, "feat", "seed.go")
415
+ writeFile(t, filepath.Join(root, "timeline.jsonl"), "before\n")
416
+ writeFile(t, filepath.Join(dir, "events.jsonl"), "before\n")
417
+ writeFile(t, filepath.Join(dir, ".a1-guard.log"), "before\n")
418
+ writeFile(t, filepath.Join(dir, ".red"), "before\n")
419
+ writeFile(t, filepath.Join(dir, "handoff.md"), "before\n")
420
+ if code := RecoveryAttempts(
421
+ root,
422
+ []string{"record", "--class", "proof_tool_defect", "reconcile telemetry", "first failure", "feat"},
423
+ &bytes.Buffer{},
424
+ &bytes.Buffer{},
425
+ ); code != 0 {
426
+ t.Fatalf("initial recovery record = %d, want 0", code)
427
+ }
428
+ if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
429
+ t.Fatalf("snapshot = %d, want 0\n%s", code, out)
430
+ }
431
+
432
+ snapshotPath := filepath.Join(dir, reconcileDevritesName)
433
+ state, err := readDevritesState(snapshotPath)
434
+ if err != nil {
435
+ t.Fatal(err)
436
+ }
437
+ have := make(map[string]bool, len(state))
438
+ for _, item := range state {
439
+ have[item.Path] = true
440
+ }
441
+ for _, path := range []string{
442
+ "timeline.jsonl",
443
+ "work/feat/events.jsonl",
444
+ "work/feat/.a1-guard.log",
445
+ "work/feat/.red",
446
+ "work/feat/handoff.md",
447
+ "work/feat/recovery-attempts.jsonl",
448
+ } {
449
+ if !have[path] {
450
+ state = append(state, devritesStateEntry{Path: path, Fingerprint: "file:0600:legacy"})
451
+ }
452
+ }
453
+ if err := writeDevritesState(snapshotPath, state); err != nil {
454
+ t.Fatal(err)
455
+ }
456
+
457
+ writeFile(t, filepath.Join(root, "timeline.jsonl"), "after\n")
458
+ writeFile(t, filepath.Join(dir, "events.jsonl"), "after\n")
459
+ writeFile(t, filepath.Join(dir, ".a1-guard.log"), "after\n")
460
+ writeFile(t, filepath.Join(dir, ".red"), "after\n")
461
+ writeFile(t, filepath.Join(dir, "handoff.md"), "after\n")
462
+ if code := RecoveryAttempts(
463
+ root,
464
+ []string{"record", "--class", "proof_tool_defect", "reconcile telemetry", "second failure", "feat"},
465
+ &bytes.Buffer{},
466
+ &bytes.Buffer{},
467
+ ); code != 0 {
468
+ t.Fatalf("second recovery record = %d, want 0", code)
469
+ }
470
+
471
+ if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
472
+ t.Fatalf("check = %d, want 0\n%s", code, out)
473
+ }
474
+ }
475
+
476
+ func TestReconcileRejectsCorruptRootOwnedRecoveryLedger(t *testing.T) {
477
+ newGitRepo(t)
478
+ root := workspace(t, "feat")
479
+ writeWrightAllowlist(t, root, "feat", "seed.go")
480
+ if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
481
+ t.Fatalf("snapshot = %d, want 0\n%s", code, out)
482
+ }
483
+ writeFile(t, filepath.Join(featureDir(root, "feat"), recoveryAttemptsFile), "{not-json}\n")
484
+
485
+ code, out := runReconcile(t, root, "check", "feat")
486
+ if code != 6 {
487
+ t.Fatalf("check = %d, want 6\n%s", code, out)
488
+ }
489
+ if !strings.Contains(out, "validate root-owned recovery ledger") {
490
+ t.Fatalf("corrupt recovery ledger missing from output:\n%s", out)
491
+ }
492
+ }
493
+
494
+ func TestReconcileRootOwnedOperationalPathsAreExact(t *testing.T) {
495
+ for _, path := range []string{
496
+ "timeline.jsonl",
497
+ "work/feat/.red",
498
+ "features/feat/events.jsonl",
499
+ } {
500
+ if !reconcileRootOwnedOperationalPath(path) {
501
+ t.Errorf("%q should be root-owned operational state", path)
502
+ }
503
+ }
504
+ for _, path := range []string{
505
+ "work/feat/.unknown.log",
506
+ "work/feat/action.log",
507
+ "work/feat/footprint.log",
508
+ "work/feat/state.md",
509
+ } {
510
+ if reconcileRootOwnedOperationalPath(path) {
511
+ t.Errorf("%q must remain fingerprinted", path)
512
+ }
513
+ }
514
+ }
515
+
364
516
  func TestReconcileCheckFailsClosedWhenObjectDatabaseIsMissing(t *testing.T) {
365
517
  newGitRepo(t)
366
518
  root := workspace(t, "feat")
@@ -583,6 +583,11 @@ func TestHookWrightScopeRequiresSpawnedCodexWorkerForSourceWrites(t *testing.T)
583
583
  runGuard(`{"tool_name":"spawn_agent","tool_input":{"agent_type":"worker"}}`, false)
584
584
  runGuard(`{"tool_name":"spawn_agent","tool_input":{"agent_type":"explorer"}}`, false)
585
585
  runGuard(`{"tool_name":"Write","tool_input":{"file_path":".devrites/features/auth-tokens/state.md"}}`, false)
586
+ runGuard(hookPayload(t, map[string]any{
587
+ "tool_name": "Write",
588
+ "tool_input": map[string]any{"file_path": filepath.Join(t.TempDir(), "agent-packet.yaml")},
589
+ }), false)
590
+ runGuard(`{"tool_name":"Write","tool_input":{"file_path":"../outside.txt"}}`, true)
586
591
  runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
587
592
  runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
588
593
  runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
@@ -1695,7 +1700,7 @@ func TestCodexAgentDispatchRejectsIncompleteDurableV2(t *testing.T) {
1695
1700
  }
1696
1701
  }
1697
1702
 
1698
- func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
1703
+ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T) {
1699
1704
  root := newWorkspace(t)
1700
1705
  writeActive(t, root, "auth-tokens")
1701
1706
  workspace := filepath.Join(root, "features", "auth-tokens")
@@ -1714,14 +1719,26 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
1714
1719
  "prompt": "$rite-build",
1715
1720
  }), nil, "hook", "agent-dispatch", "--harness=codex")
1716
1721
 
1717
- reconcile := hookPayload(t, map[string]any{
1722
+ check := hookPayload(t, map[string]any{
1723
+ "hook_event_name": "PreToolUse",
1724
+ "session_id": sessionID,
1725
+ "turn_id": turnID,
1726
+ "tool_name": "Bash",
1727
+ "tool_input": map[string]any{"command": "rtk devrites-engine reconcile check"},
1728
+ })
1729
+ if out, stderr, code := runDevritesIO(t, root, check, nil,
1730
+ "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1731
+ t.Fatalf("ordinary reconcile check exit=%d out=%s stderr=%s", code, out, stderr)
1732
+ }
1733
+
1734
+ closeWindow := hookPayload(t, map[string]any{
1718
1735
  "hook_event_name": "PreToolUse",
1719
1736
  "session_id": sessionID,
1720
1737
  "turn_id": turnID,
1721
1738
  "tool_name": "Bash",
1722
1739
  "tool_input": map[string]any{"command": "rtk devrites-engine reconcile close"},
1723
1740
  })
1724
- out, _, _ := runDevritesIO(t, root, reconcile, nil,
1741
+ out, _, _ := runDevritesIO(t, root, closeWindow, nil,
1725
1742
  "hook", "agent-dispatch", "--harness=codex")
1726
1743
  if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
1727
1744
  !strings.Contains(reason, "confirmed") {
@@ -1774,7 +1791,7 @@ func TestCodexAgentDispatchGatesReconcileOnBoundWrightResult(t *testing.T) {
1774
1791
  }
1775
1792
  }
1776
1793
 
1777
- if out, stderr, code := runDevritesIO(t, root, reconcile, nil,
1794
+ if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
1778
1795
  "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1779
1796
  t.Fatalf("completed wright reconcile exit=%d out=%s stderr=%s", code, out, stderr)
1780
1797
  }
@@ -26,8 +26,8 @@ only the source-write lifecycle.
26
26
  4. Immediately before a serial dispatch:
27
27
 
28
28
  ```bash
29
- devrites-engine reconcile snapshot
30
29
  devrites-engine stuck log "$(cat .devrites/ACTIVE 2>/dev/null)" dispatch "<slice id>"
30
+ devrites-engine reconcile snapshot
31
31
  ```
32
32
 
33
33
  A Forge slice runs `forge plan` first, then takes this same snapshot; see
@@ -26,8 +26,8 @@ only the source-write lifecycle.
26
26
  4. Immediately before a serial dispatch:
27
27
 
28
28
  ```bash
29
- devrites-engine reconcile snapshot
30
29
  devrites-engine stuck log "$(cat .devrites/ACTIVE 2>/dev/null)" dispatch "<slice id>"
30
+ devrites-engine reconcile snapshot
31
31
  ```
32
32
 
33
33
  A Forge slice runs `forge plan` first, then takes this same snapshot; see
@@ -26,8 +26,8 @@ only the source-write lifecycle.
26
26
  4. Immediately before a serial dispatch:
27
27
 
28
28
  ```bash
29
- devrites-engine reconcile snapshot
30
29
  devrites-engine stuck log "$(cat .devrites/ACTIVE 2>/dev/null)" dispatch "<slice id>"
30
+ devrites-engine reconcile snapshot
31
31
  ```
32
32
 
33
33
  A Forge slice runs `forge plan` first, then takes this same snapshot; see
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.2.7",
3
+ "version": "3.2.9",
4
4
  "description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code and Codex",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://github.com/ViktorsBaikers/DevRites#readme",