devrites 3.2.10 → 3.2.11

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,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
+ ## [3.2.11](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.10...v3.2.11) (2026-07-26)
6
+
7
+ ### Fixed
8
+
9
+ * **devrites:** bind canonical state at wright start ([b58a531](https://github.com/ViktorsBaikers/DevRites/commit/b58a531749b94f734b7abf7469656ad543a0cfa0))
10
+
5
11
  ## [3.2.10](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.9...v3.2.10) (2026-07-26)
6
12
 
7
13
  ### 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.10`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.10): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
28
+ **Status:** [`v3.2.11`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.11): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -62,11 +62,16 @@ Each dispatch follows the retained-baseline sequence:
62
62
 
63
63
  1. `reconcile snapshot` captures the original dirty-tree baseline, private Git
64
64
  objects, exact allowlist, and canonical-state fingerprint.
65
- 2. The sole wright returns code/tests; the root validates its typed identity and
65
+ 2. The confirmed wright start captures a second canonical-state fingerprint.
66
+ This preserves the original source baseline while separating root-owned
67
+ recovery records that predate the writer from canonical mutations made while
68
+ the writer is active.
69
+ 3. The sole wright returns code/tests; the root validates its typed identity and
66
70
  exact changed-file set.
67
- 3. `reconcile check` rejects anything outside the root allowlist.
68
- 4. `test-integrity` runs against the retained baseline.
69
- 5. After proof and decision checks pass, `reconcile close` retires the private
71
+ 4. `reconcile check` rejects anything outside the root allowlist and any
72
+ canonical-state change after the wright start.
73
+ 5. `test-integrity` runs against the retained baseline.
74
+ 6. After proof and decision checks pass, `reconcile close` retires the private
70
75
  window; only then does the root write canonical records.
71
76
 
72
77
  An ordinary `reconcile check` may revalidate an existing retained window
package/engine/hooks.go CHANGED
@@ -546,6 +546,7 @@ func hookSubagentOrient(h harness.Harness, stdin io.Reader, stdout, stderr io.Wr
546
546
  }
547
547
  agentType := h.SubagentAgentType(bytes.NewReader(data))
548
548
  context := strings.ReplaceAll(subagentOrientContext, "\r\n", "\n")
549
+ confirmedRole := ""
549
550
  if h == harness.Codex {
550
551
  in, parseErr := parseAgentDispatchHookInput(bytes.NewReader(data))
551
552
  root, ok := agentDispatchRoot()
@@ -559,6 +560,7 @@ func hookSubagentOrient(h harness.Harness, stdin io.Reader, stdout, stderr io.Wr
559
560
  if bindErr != nil {
560
561
  context += "\n\n- **Dispatch binding failed.** Return blocked without doing specialist work; the parent must repair the agent-start receipt."
561
562
  } else if bound {
563
+ confirmedRole = role
562
564
  if in.AgentType == role {
563
565
  context += fmt.Sprintf(
564
566
  "\n\n- **Confirmed role.** Your named Codex profile is `%s`; follow its loaded `developer_instructions` and execute only the unchanged packet from the parent.",
@@ -578,6 +580,17 @@ func hookSubagentOrient(h harness.Harness, stdin io.Reader, stdout, stderr io.Wr
578
580
  }
579
581
  } else if !strings.HasPrefix(agentType, "devrites-") {
580
582
  return exitOK // Stay silent without a DevRites agent identity.
583
+ } else {
584
+ confirmedRole = agentType
585
+ }
586
+ if confirmedRole == "devrites-slice-wright" {
587
+ root, slug, _, ok := resolveWorkspace()
588
+ if !ok {
589
+ context += "\n\n- **Wright boundary unavailable.** Return blocked without writing; the parent must restore the active reconcile window."
590
+ } else if err := lib.CaptureReconcileWrightBoundary(root, slug); err != nil {
591
+ debugf(stderr, "subagent-orient wright boundary: %v", err)
592
+ context += "\n\n- **Wright boundary unavailable.** Return blocked without writing; the parent must repair the retained reconcile window."
593
+ }
581
594
  }
582
595
  out, err := h.SubagentStartContext(context)
583
596
  if err != nil {
@@ -400,6 +400,8 @@ func currentReconcileWindowID() string {
400
400
  if !ok {
401
401
  return ""
402
402
  }
403
+ // The wright-start canonical fingerprint is captured after the pending
404
+ // receipt. It narrows the delta but does not change source authorization.
403
405
  names := []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"}
404
406
  h := sha256.New()
405
407
  for _, name := range names {
@@ -23,6 +23,7 @@ const (
23
23
  reconcileCheckedName = ".reconcile-checked"
24
24
  reconcileDevritesName = ".reconcile-devrites"
25
25
  reconcileObjectsName = ".reconcile-objects"
26
+ reconcileWrightStateName = ".reconcile-wright-devrites"
26
27
  defaultWrightAllowlistName = ".wright-allowlist"
27
28
  wrightAllowlistFileEnv = "DEVRITES_WRIGHT_ALLOWLIST_FILE"
28
29
  reconcileDevritesPathPrefix = ".devrites/"
@@ -30,9 +31,11 @@ const (
30
31
 
31
32
  // Reconcile enforces the source-write boundary around one dispatched
32
33
  // slice-wright. The first `snapshot` captures the dirty worktree, private object
33
- // database, exact orchestrator-authored allowlist, and .devrites state. After a
34
- // clean check, another snapshot re-arms only the dispatch state for a retry while
35
- // retaining the original source baseline.
34
+ // database, exact orchestrator-authored allowlist, and .devrites state. A
35
+ // confirmed wright start captures the current canonical state separately, so
36
+ // retained-window recovery records are not attributed to the writer. After a
37
+ // clean check, another snapshot re-arms only the dispatch state for a retry
38
+ // while retaining the original source baseline.
36
39
  // `check` compares the captured state with the current state and retains the
37
40
  // immutable baseline for the later test-integrity gate.
38
41
  // `close` explicitly ends the window and removes its private artifacts.
@@ -62,10 +65,11 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
62
65
  devritesSnapshot := filepath.Join(d, reconcileDevritesName)
63
66
  objects := filepath.Join(d, reconcileObjectsName)
64
67
  checked := filepath.Join(d, reconcileCheckedName)
68
+ wrightState := filepath.Join(d, reconcileWrightStateName)
65
69
 
66
70
  closeWindow := func() error {
67
71
  var failures []string
68
- for _, privateFile := range []string{base, capturedAllowlist, devritesSnapshot, checked} {
72
+ for _, privateFile := range []string{base, capturedAllowlist, devritesSnapshot, checked, wrightState} {
69
73
  if err := os.Remove(privateFile); err != nil && !os.IsNotExist(err) {
70
74
  failures = append(failures, fmt.Sprintf("%s: %v", filepath.Base(privateFile), err))
71
75
  }
@@ -131,7 +135,7 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
131
135
  fmt.Fprintf(stderr, "reconcile: cannot refresh wright allowlist: %v\n", err)
132
136
  return 6
133
137
  }
134
- state, err := captureReconcileDevritesState(root, devritesSnapshot, activeObjects, checked)
138
+ state, err := captureReconcileDevritesState(root, devritesSnapshot, activeObjects, checked, wrightState)
135
139
  if err != nil {
136
140
  fmt.Fprintf(stderr, "reconcile: cannot refresh .devrites snapshot: %v\n", err)
137
141
  return 6
@@ -140,9 +144,11 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
140
144
  fmt.Fprintf(stderr, "reconcile: cannot write refreshed .devrites snapshot: %v\n", err)
141
145
  return 6
142
146
  }
143
- if err := os.Remove(checked); err != nil {
144
- fmt.Fprintf(stderr, "reconcile: cannot arm refreshed slice window: %v\n", err)
145
- return 6
147
+ for _, privateFile := range []string{checked, wrightState} {
148
+ if err := os.Remove(privateFile); err != nil && !os.IsNotExist(err) {
149
+ fmt.Fprintf(stderr, "reconcile: cannot arm refreshed slice window: %v\n", err)
150
+ return 6
151
+ }
146
152
  }
147
153
  fmt.Fprintf(stdout, "reconcile: dispatch snapshot refreshed for %s; original slice baseline retained.\n", slug)
148
154
  return 0
@@ -180,7 +186,7 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
180
186
  fmt.Fprintf(stderr, "reconcile: cannot capture wright allowlist: %v\n", err)
181
187
  return 6
182
188
  }
183
- state, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked)
189
+ state, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked, wrightState)
184
190
  if err != nil {
185
191
  _ = closeWindow()
186
192
  fmt.Fprintf(stderr, "reconcile: cannot snapshot .devrites: %v\n", err)
@@ -210,13 +216,20 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
210
216
  fmt.Fprintf(stderr, "reconcile: invalid captured allowlist: %v\n", err)
211
217
  return 6
212
218
  }
213
- beforeDevrites, err := readDevritesState(devritesSnapshot)
219
+ beforeDevritesPath := devritesSnapshot
220
+ if _, err := os.Lstat(wrightState); err == nil {
221
+ beforeDevritesPath = wrightState
222
+ } else if !os.IsNotExist(err) {
223
+ fmt.Fprintf(stderr, "reconcile: invalid wright-start .devrites snapshot: %v\n", err)
224
+ return 6
225
+ }
226
+ beforeDevrites, err := readDevritesState(beforeDevritesPath)
214
227
  if err != nil {
215
228
  fmt.Fprintf(stderr, "reconcile: invalid .devrites snapshot: %v\n", err)
216
229
  return 6
217
230
  }
218
231
  beforeDevrites = withoutRootOwnedOperationalState(beforeDevrites)
219
- afterDevrites, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked)
232
+ afterDevrites, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked, wrightState)
220
233
  if err != nil {
221
234
  fmt.Fprintf(stderr, "reconcile: cannot compare .devrites: %v\n", err)
222
235
  return 6
@@ -274,6 +287,50 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
274
287
  }
275
288
  }
276
289
 
290
+ // CaptureReconcileWrightBoundary records canonical state at the confirmed
291
+ // wright start. The original source baseline remains untouched; reconciliation
292
+ // can therefore distinguish root recovery records that predate the writer from
293
+ // canonical mutations made while the writer is active.
294
+ func CaptureReconcileWrightBoundary(root, slug string) error {
295
+ d := featureDir(root, slug)
296
+ if slug == "" || !isDir(d) {
297
+ return fmt.Errorf("no active workspace")
298
+ }
299
+ base := filepath.Join(d, reconcileBaseName)
300
+ capturedAllowlist := filepath.Join(d, reconcileAllowlistName)
301
+ devritesSnapshot := filepath.Join(d, reconcileDevritesName)
302
+ objects := filepath.Join(d, reconcileObjectsName)
303
+ checked := filepath.Join(d, reconcileCheckedName)
304
+ wrightState := filepath.Join(d, reconcileWrightStateName)
305
+
306
+ for _, filename := range []string{base, capturedAllowlist} {
307
+ info, err := os.Lstat(filename)
308
+ if err != nil || !info.Mode().IsRegular() {
309
+ if err == nil {
310
+ err = fmt.Errorf("not a regular file")
311
+ }
312
+ return fmt.Errorf("invalid reconcile window %s: %w", filepath.Base(filename), err)
313
+ }
314
+ }
315
+ if !isDir(objects) {
316
+ return fmt.Errorf("invalid reconcile window %s: not a directory", filepath.Base(objects))
317
+ }
318
+ if _, err := readDevritesState(devritesSnapshot); err != nil {
319
+ return fmt.Errorf("invalid reconcile window %s: %w", filepath.Base(devritesSnapshot), err)
320
+ }
321
+ state, err := captureReconcileDevritesState(
322
+ root,
323
+ devritesSnapshot,
324
+ objects,
325
+ checked,
326
+ wrightState,
327
+ )
328
+ if err != nil {
329
+ return err
330
+ }
331
+ return writeDevritesState(wrightState, state)
332
+ }
333
+
277
334
  func wrightAllowlistPath(gitRoot, workspace string) (string, error) {
278
335
  configured := os.Getenv(wrightAllowlistFileEnv)
279
336
  var filename string
@@ -361,6 +361,49 @@ func TestReconcileRejectsDevritesMutation(t *testing.T) {
361
361
  }
362
362
  }
363
363
 
364
+ func TestReconcileUsesWrightStartAsCanonicalStateBoundary(t *testing.T) {
365
+ gitRoot := newGitRepo(t)
366
+ root := workspace(t, "feat")
367
+ dir := featureDir(root, "feat")
368
+ writeWrightAllowlist(t, root, "feat", "seed.go")
369
+ if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
370
+ t.Fatalf("snapshot = %d, want 0\n%s", code, out)
371
+ }
372
+
373
+ // Root-owned canonical records may legitimately change while a retained
374
+ // source baseline survives technical recovery. The next wright start is the
375
+ // boundary that separates those changes from the writer's delta.
376
+ for _, name := range []string{
377
+ "action.log",
378
+ "browser-evidence.md",
379
+ "decisions.md",
380
+ "evidence.md",
381
+ "footprint.log",
382
+ "state.md",
383
+ "touched-files.md",
384
+ } {
385
+ writeFile(t, filepath.Join(dir, name), "Root-owned recovery record\n")
386
+ }
387
+ if err := CaptureReconcileWrightBoundary(root, "feat"); err != nil {
388
+ t.Fatalf("capture wright boundary: %v", err)
389
+ }
390
+ writeFile(t, filepath.Join(gitRoot, "seed.go"), "package main\n\nfunc repaired() {}\n")
391
+
392
+ if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
393
+ t.Fatalf("check = %d, want 0\n%s", code, out)
394
+ }
395
+
396
+ // A canonical mutation after the wright starts is still a violation.
397
+ writeFile(t, filepath.Join(dir, "state.md"), "Phase: prove\n")
398
+ code, out := runReconcile(t, root, "check", "feat")
399
+ if code != 5 {
400
+ t.Fatalf("check after writer-time canonical mutation = %d, want 5\n%s", code, out)
401
+ }
402
+ if !strings.Contains(out, ".devrites/work/feat/state.md") {
403
+ t.Fatalf("writer-time canonical mutation missing from output:\n%s", out)
404
+ }
405
+ }
406
+
364
407
  func TestReconcileAllowsRootOwnedOperationalFiles(t *testing.T) {
365
408
  newGitRepo(t)
366
409
  root := workspace(t, "feat")
@@ -613,6 +656,7 @@ func TestReconcileRefreshPreservesOriginalBaselineAcrossRetry(t *testing.T) {
613
656
  // between attempts. Refresh must re-arm those boundaries without replacing
614
657
  // the original pre-slice source baseline.
615
658
  writeFile(t, filepath.Join(featureDir(root, "feat"), "recovery-attempts.json"), "{}\n")
659
+ writeFile(t, filepath.Join(featureDir(root, "feat"), reconcileWrightStateName), "stale\n")
616
660
  writeWrightAllowlist(t, root, "feat", "seed.go", "retry.go")
617
661
  if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
618
662
  t.Fatalf("refresh snapshot = %d, want 0\n%s", code, out)
@@ -626,6 +670,9 @@ func TestReconcileRefreshPreservesOriginalBaselineAcrossRetry(t *testing.T) {
626
670
  if !bytes.Equal(originalBase, refreshedBase) {
627
671
  t.Fatalf("refresh replaced original baseline: before=%q after=%q", originalBase, refreshedBase)
628
672
  }
673
+ if isFile(filepath.Join(featureDir(root, "feat"), reconcileWrightStateName)) {
674
+ t.Fatal("refresh retained the prior wright-start boundary")
675
+ }
629
676
 
630
677
  writeFile(t, filepath.Join(gitRoot, "retry.go"), "package main\n")
631
678
  if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
@@ -655,6 +702,7 @@ func TestReconcileCloseClearsPrivateWindowState(t *testing.T) {
655
702
  newGitRepo(t)
656
703
  root := workspace(t, "feat")
657
704
  writeFile(t, filepath.Join(root, "work", "feat", reconcileBaseName), "stale\n")
705
+ writeFile(t, filepath.Join(root, "work", "feat", reconcileWrightStateName), "stale\n")
658
706
  if err := os.MkdirAll(filepath.Join(root, "work", "feat", reconcileObjectsName), 0o755); err != nil {
659
707
  t.Fatal(err)
660
708
  }
@@ -666,7 +714,9 @@ func TestReconcileCloseClearsPrivateWindowState(t *testing.T) {
666
714
  if !strings.Contains(out, "closed slice window") {
667
715
  t.Errorf("expected close message:\n%s", out)
668
716
  }
669
- if isFile(filepath.Join(featureDir(root, "feat"), reconcileBaseName)) || isDir(filepath.Join(featureDir(root, "feat"), reconcileObjectsName)) {
717
+ if isFile(filepath.Join(featureDir(root, "feat"), reconcileBaseName)) ||
718
+ isFile(filepath.Join(featureDir(root, "feat"), reconcileWrightStateName)) ||
719
+ isDir(filepath.Join(featureDir(root, "feat"), reconcileObjectsName)) {
670
720
  t.Fatal("close retained private baseline state")
671
721
  }
672
722
  }
@@ -926,6 +926,44 @@ func TestHookSubagentOrientSilentForNonDevritesAgent(t *testing.T) {
926
926
  }
927
927
  }
928
928
 
929
+ func TestHookSubagentOrientCapturesWrightCanonicalBoundary(t *testing.T) {
930
+ root := newWorkspace(t)
931
+ writeActive(t, root, "auth-tokens")
932
+ workspace := filepath.Join(root, "features", "auth-tokens")
933
+ if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
934
+ t.Fatal(err)
935
+ }
936
+ for name, body := range map[string]string{
937
+ ".reconcile-base": "base\n",
938
+ ".reconcile-allowlist": "",
939
+ ".reconcile-devrites": "[]\n",
940
+ "state.md": "Phase: build\n",
941
+ "browser-evidence.md": "Prior root evidence\n",
942
+ } {
943
+ if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
944
+ t.Fatal(err)
945
+ }
946
+ }
947
+
948
+ in := `{"agent_type":"devrites-slice-wright"}`
949
+ out, errOut, code := runDevritesIO(t, root, in, nil, "hook", "subagent-orient", "--harness=claude")
950
+ if code != 0 {
951
+ t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
952
+ }
953
+ if strings.Contains(out, "Wright boundary unavailable") {
954
+ t.Fatalf("wright start was blocked: %s", out)
955
+ }
956
+ raw, err := os.ReadFile(filepath.Join(workspace, ".reconcile-wright-devrites"))
957
+ if err != nil {
958
+ t.Fatalf("read wright boundary: %v", err)
959
+ }
960
+ for _, path := range []string{"features/auth-tokens/state.md", "features/auth-tokens/browser-evidence.md"} {
961
+ if !strings.Contains(string(raw), path) {
962
+ t.Errorf("wright boundary omitted %s: %s", path, raw)
963
+ }
964
+ }
965
+ }
966
+
929
967
  func hookPayload(t *testing.T, value map[string]any) string {
930
968
  t.Helper()
931
969
  raw, err := json.Marshal(value)
@@ -1759,11 +1797,18 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
1759
1797
  root := newWorkspace(t)
1760
1798
  writeActive(t, root, "auth-tokens")
1761
1799
  workspace := filepath.Join(root, "features", "auth-tokens")
1762
- for _, name := range []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"} {
1763
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(name+"\n"), 0o600); err != nil {
1800
+ for name, body := range map[string]string{
1801
+ ".reconcile-base": ".reconcile-base\n",
1802
+ ".reconcile-allowlist": ".reconcile-allowlist\n",
1803
+ ".reconcile-devrites": "[]\n",
1804
+ } {
1805
+ if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
1764
1806
  t.Fatal(err)
1765
1807
  }
1766
1808
  }
1809
+ if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
1810
+ t.Fatal(err)
1811
+ }
1767
1812
  sessionID, turnID := "session-wright", "turn-wright"
1768
1813
  writeCodexAgentContract(t, root, "devrites-slice-wright")
1769
1814
  writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
@@ -1808,7 +1853,7 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
1808
1853
  "tool_name": "spawn_agent",
1809
1854
  "tool_use_id": "spawn-wright",
1810
1855
  "tool_input": map[string]any{
1811
- "agent_type": "worker",
1856
+ "agent_type": "devrites-slice-wright",
1812
1857
  "fork_turns": "none",
1813
1858
  "message": "Read .codex/agents/devrites-slice-wright.toml.",
1814
1859
  },
@@ -1818,14 +1863,14 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
1818
1863
  "session_id": sessionID,
1819
1864
  "turn_id": "child-wright",
1820
1865
  "agent_id": "agent-wright",
1821
- "agent_type": "worker",
1866
+ "agent_type": "devrites-slice-wright",
1822
1867
  },
1823
1868
  {
1824
1869
  "hook_event_name": "SubagentStop",
1825
1870
  "session_id": sessionID,
1826
1871
  "turn_id": "child-wright",
1827
1872
  "agent_id": "agent-wright",
1828
- "agent_type": "worker",
1873
+ "agent_type": "devrites-slice-wright",
1829
1874
  "last_assistant_message": "agent-result/v1 complete",
1830
1875
  },
1831
1876
  {
@@ -1845,6 +1890,9 @@ func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T)
1845
1890
  t.Fatalf("%s exit=%d stderr=%s", hook, code, stderr)
1846
1891
  }
1847
1892
  }
1893
+ if _, err := os.Stat(filepath.Join(workspace, ".reconcile-wright-devrites")); err != nil {
1894
+ t.Fatalf("wright start did not capture canonical boundary: %v", err)
1895
+ }
1848
1896
 
1849
1897
  if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
1850
1898
  "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
@@ -120,10 +120,12 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
120
120
 
121
121
  ## Host dispatch
122
122
 
123
- Use named wright → enforced V1 generic worker from `standards/agents.md`. On Codex
124
- MultiAgent V2, dispatch the exact named `devrites-slice-wright` with a unique
125
- `task_name` and `fork_turns="none"`; its durable child rollout must prove the native
126
- wright role, wait, completion, and non-empty result from the current reconcile window.
123
+ Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
124
+ dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
125
+ `fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
126
+ result in the current window. The confirmed start fingerprints canonical state apart
127
+ from the retained source baseline: pre-start root recovery records are excluded, while
128
+ post-start canonical changes fail.
127
129
  Any generic worker still needs the exact allowlist or an isolated checkout;
128
130
  reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
129
131
  for HITL. On Codex, `reconcile check` and `reconcile close` require the completed
@@ -120,10 +120,12 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
120
120
 
121
121
  ## Host dispatch
122
122
 
123
- Use named wright → enforced V1 generic worker from `standards/agents.md`. On Codex
124
- MultiAgent V2, dispatch the exact named `devrites-slice-wright` with a unique
125
- `task_name` and `fork_turns="none"`; its durable child rollout must prove the native
126
- wright role, wait, completion, and non-empty result from the current reconcile window.
123
+ Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
124
+ dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
125
+ `fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
126
+ result in the current window. The confirmed start fingerprints canonical state apart
127
+ from the retained source baseline: pre-start root recovery records are excluded, while
128
+ post-start canonical changes fail.
127
129
  Any generic worker still needs the exact allowlist or an isolated checkout;
128
130
  reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
129
131
  for HITL. On Codex, `reconcile check` and `reconcile close` require the completed
@@ -120,10 +120,12 @@ resume the same slice. Close it before abandoning the slice for a scope/plan tra
120
120
 
121
121
  ## Host dispatch
122
122
 
123
- Use named wright → enforced V1 generic worker from `standards/agents.md`. On Codex
124
- MultiAgent V2, dispatch the exact named `devrites-slice-wright` with a unique
125
- `task_name` and `fork_turns="none"`; its durable child rollout must prove the native
126
- wright role, wait, completion, and non-empty result from the current reconcile window.
123
+ Use named wright → enforced V1 generic worker from `standards/agents.md`. Codex V2
124
+ dispatches the exact named `devrites-slice-wright` with a unique `task_name` and
125
+ `fork_turns="none"`; its durable rollout proves the native role, wait, and non-empty
126
+ result in the current window. The confirmed start fingerprints canonical state apart
127
+ from the retained source baseline: pre-start root recovery records are excluded, while
128
+ post-start canonical changes fail.
127
129
  Any generic worker still needs the exact allowlist or an isolated checkout;
128
130
  reconciliation alone is insufficient. If no safe fresh-agent rung is available, stop
129
131
  for HITL. On Codex, `reconcile check` and `reconcile close` require the completed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "3.2.10",
3
+ "version": "3.2.11",
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",